cipi api

Cipi can optionally enable a REST API layer on the server via cipi api <domain>. It is powered by the Laravel package cipi/api (current release 1.31.0, which needs Cipi 5.4.1+ for its newest endpoints), which exposes:

  • REST API — apps (including Octane and Node create, .env, auth.json, Artisan, whitelisted app run, deploy-config, fix-permissions), aliases, www redirects, app and path redirects, prefix proxies, Node runtimes, deploy and the deploy audit ledger, SSL, multi-engine databases, Meilisearch, app logs, server status (/api/*)
  • MCP server — 68 tools at /mcp (Streamable HTTP)
  • Read-only host insights — optional packages, system monitor, Cloudflare Zero Trust status (API 1.31+)
  • Server cockpit — PHP install/switch, SSH keys, services, SMTP, healthchecks, IP whitelist (API 1.15.0+ / Cipi 5.0.6+)
  • Swagger UI — interactive reference at /docs

Requires PHP 8.2+ and Laravel 12+ on the panel host. This is server-level automation — distinct from the per-app Cipi Agent package (cipi/agent on each Laravel app).

The cipi/api package

On a normal Cipi server you never install the package manually — cipi api <domain> provisions Laravel at /opt/cipi/api, Nginx, SSL, SQLite job queue, and cipi-queue.service. For reference or custom setups:

bash
$ composer require cipi/api
$ php artisan vendor:publish --tag=cipi-config
$ php artisan vendor:publish --tag=cipi-assets
$ php artisan migrate
$ php artisan cipi:seed-api-user
$ php artisan cipi:token-create

Panel .env uses CIPI_APPS_JSON=/etc/cipi/apps.json (or apps-public.json projection for non-sensitive fields). Token abilities are defined in config/cipi.php — list them with php artisan cipi:token-abilities (same list as cipi api token create since Cipi 4.6.3).

Source and changelog: github.com/cipi-sh/api (MIT). Client wrapper: cipi-cli.

Commands

bash
$ cipi api <domain>           # configure API at root (e.g. api.myhosting.com)
$ cipi api ssl                  # install Let's Encrypt certificate for API domain
$ cipi api token list           # list tokens
$ cipi api token create         # create a new token (choose abilities)
$ cipi api token revoke <id>    # revoke a token
$ cipi api status               # Laravel + cipi-api versions, queue worker, pending jobs, FPM pool
$ cipi api fix-permissions      # repair panel storage/database ownership (www-data)
$ cipi api update               # soft update: composer update on Laravel and API packages
$ cipi api upgrade              # full rebuild with rollback at /opt/cipi/api.old

Panel API troubleshooting

After cipi self-update, root-owned files under /opt/cipi/api or /opt/cipi/gui can prevent PHP-FPM (www-data) from writing logs or the SQLite job database — the browser shows a bare HTTP 500 on /docs or /mcp. Cipi normally repairs ownership automatically during self-update (migration 5.0.13+ reclaims API/GUI ownership); if problems persist:

bash
$ cipi api fix-permissions   # chown storage, database, bootstrap/cache, .env → www-data
$ cipi api status              # confirm Laravel version, queue worker, pending jobs

cipi api status prints the installed Laravel and cipi/api package versions, whether cipi-queue.service is active, pending async jobs in the panel SQLite database, and PHP-FPM pool stats for the API vhost (including slow requests when configured). cipi api update soft-updates the panel API package from Packagist (since 5.0.15; migration drops stale VCS repo entries). cipi api upgrade performs a full rebuild with rollback at /opt/cipi/api.old. Since 5.0.14–5.0.17, self-update uses timed GitHub tarballs and Packagist dist installs instead of blocking Composer VCS clones for API and GUI packages. Since 5.0.18, GUI upgrade/update avoids Composer symlinks that break PHP-FPM open_basedir (HTTP 500); run cipi gui fix-permissions or cipi self-update to repair existing panels.

Since v4.7.18 (migrations 4.7.15–4.7.18), Panel API failures on Ubuntu 25.10+ / 26.04 are fixed end-to-end: sudo-rs rejects cipi db restore * * wildcards (the whole sudoers file was ignored — I'm afraid I can't do that), so the whitelist uses trailing * only; sourcing common.sh no longer aborts read-only commands when /etc/cipi is remounted read-only; API open_basedir includes /usr/local/bin/ for log helpers; and cipi db list shows empty databases and surfaces vault/MariaDB errors. Run cipi self-update to apply.

Token creation and granular permissions

Authentication uses Sanctum. Each token can have one or more abilities that limit allowed operations:

  • apps-view — read apps
  • apps-create — create apps
  • apps-edit — edit apps (PHP, repository, branch, primary domain since API 1.9.0+ / Cipi 4.6.2+)
  • apps-suspend — suspend and unsuspend apps
  • apps-basicauth — enable, disable, and inspect HTTP Basic Auth on apps (API 1.10.0+)
  • apps-env — list/merge app .env keys (API 1.14.0+ / Cipi 5.0.3+)
  • apps-auth — manage shared Composer auth.json (API 1.14.0+; distinct from apps-basicauth)
  • apps-artisan — run Artisan as an async job (API 1.14.0+)
  • apps-run — whitelisted non-interactive app run (API 1.14.0+)
  • apps-deploy-config — structured Deployer recipe options (API 1.14.0+)
  • php-view — list installed PHP versions (API 1.15.0+)
  • php-manage — install PHP versions (API 1.15.0+; install-only since 1.19.0 — removing a version or switching the default stays on the CLI)
  • ssh-view — list SSH keys on the cipi user (API 1.15.0+)
  • ssh-manage — add/remove/rename SSH keys (API 1.15.0+)
  • services-view — list system services (API 1.15.0+)
  • services-manage — restart services (API 1.15.0+)
  • smtp-view — read SMTP notification settings (password never returned; API 1.15.0+)
  • smtp-manage — configure, enable, disable, test, delete SMTP (API 1.15.0+)
  • health-view — list healthchecks (API 1.15.0+)
  • health-manage — set, unset, and run healthchecks per app (API 1.15.0+)
  • ip-whitelist-view — read panel API / MCP IP allowlist (API 1.15.0+)
  • ip-whitelist-manage — edit IP allowlist entries (API 1.15.0+)
  • redirects-view / redirects-manage — read and change whole-app and path redirects (API 1.31.0+ / Cipi 5.4.1+)
  • proxies-view / proxies-manage — read and change prefix reverse proxies (API 1.31.0+ / Cipi 5.4.1+)
  • node-view — Node runtimes and per-app Node status; node-manage — blue/green restart of an SSR app (API 1.31.0+ / Cipi 5.4.1+)
  • search-view — Meilisearch status; search-manage — enable or disable search per Laravel app (API 1.31.0+ / Cipi 5.2.2+)
  • packages-view, monitor-view, zt-view — read-only host package catalog, system monitor and Cloudflare Zero Trust status (API 1.31.0+)
  • apps-delete — delete apps
  • deploy-manage — deploy, rollback, unlock, and read the deploy audit ledger (API 1.31.0+ / Cipi 5.4.0+)
  • ssl-manage — install and manage SSL certificates
  • aliases-view — read aliases
  • aliases-create — add aliases
  • aliases-delete — remove aliases
  • www-manage — www/apex counterpart and redirects (API 1.12.0+ / Cipi 4.8+)
  • dbs-view — list databases
  • dbs-create — create databases
  • dbs-manage — backup, restore, regenerate password, install an engine
  • status-view — read server status snapshot (GET /api/status, API 1.11.6+)
  • mcp-access — access the MCP server

Since Cipi 4.6.3 / API 1.11.7+, cipi api token create reads the canonical ability list from the panel API package (same entries as php artisan cipi:token-abilities on the server). Migration 4.6.3 retrofits existing servers with the updated list (including status-view, apps-suspend, and apps-basicauth). API 1.19.0 removed dbs-delete along with the endpoints it guarded.

REST endpoints

All endpoints require the Authorization: Bearer <token> header. Write operations (create, edit, delete, deploy, rollback, unlock, SSL, alias, www, database) are asynchronous: they return 202 Accepted with a job_id to poll via GET /api/jobs/{id}. Read-only endpoints such as GET /api/dbs, GET /api/dbs/engines, GET /api/status, GET /api/apps/{name}/www, and GET /api/apps/{name}/logs (API 1.11.9+), GET /api/php, GET /api/ssh/keys, GET /api/services, GET /api/smtp, GET /api/health, GET /api/ip-whitelist (API 1.15.0+ / Cipi 5.0.6+), and the API 1.31.0 additions — redirects, proxies, GET /api/node, the deploy audit, search, packages, monitor and Zero Trust — are synchronous. POST /api/apps accepts optional custom (boolean) and docroot (string) parameters for creating custom apps with classic deployment. The repository field is required for Laravel apps and optional for custom apps: omit it (or send empty) to provision an SFTP-only site aligned with Cipi v4.5.1+. When no repository is set, branch is omitted. Since API 1.12.0+ / Cipi 4.8+, Laravel app create also accepts optional engine (mariadb or pgsql) to pick the database engine. Since API 1.13.0+ / Cipi 5.0+, Laravel app create accepts optional octane (true or "frankenphp") to provision Laravel Octane (FrankenPHP); octane is rejected when custom is set. The MCP tool AppCreate follows the same rules. Keep the API package current with cipi api update / cipi api upgrade so validation and OpenAPI match this behavior.

POST /api/apps/{name}/suspend takes an app offline by swapping its Nginx vhost for a generic HTTP 503 maintenance page (HTTPS included) without deleting it, while POST /api/apps/{name}/unsuspend restores the normal vhost. Both require the apps-suspend ability and return 409 if the app is already in the target state. The suspended flag survives vhost regeneration and is exposed on GET /api/apps and GET /api/apps/{name}. These endpoints require the API package 1.8.1+ and Cipi 4.5.8+ on the server.

PUT /api/apps/{name} accepts an optional domain field to rename the app’s primary domain. Since API 1.15.0+ / Cipi 5.0.6+, the endpoint only forwards fields that differ from the current app (prevents no-op webhook or deploy-key recreation when PHP or branch are unchanged). PHP must be installed on the host (422 otherwise). The API validates the format synchronously and returns 409 if the domain is already used by another app (aliases of the current app are allowed, so promoting an alias to primary works). Requires the API package 1.9.0+ and Cipi 4.6.2+. The MCP tool AppEdit accepts the same domain parameter.

GET /api/apps and GET /api/apps/{name} expose boolean suspended and basic_auth flags per app (from apps.json). Since API 1.12.0+ they also expose engine, www_redirect, and force_https from apps-public.json / apps metadata. Since API 1.13.0+ they expose octane and octane_port for Octane apps.

HTTP Basic Auth endpoints under /api/apps/{name}/basicauth/* wrap cipi basicauth synchronously — they do not return a job_id. Enable accepts optional user and password (auto-generated when omitted; returned once in the response). Requires the apps-basicauth ability and API package 1.10.0+. This is distinct from Composer auth.json management — see cipi basicauth.

WWW / apex endpoints under /api/apps/{name}/www/* wrap cipi www (API 1.12.0+ / Cipi 4.8+). GET …/www is synchronous and returns primary, apex, www, and redirect. POST …/www/add, …/force-to-root, …/force-from-root, and …/clear are async jobs. Requires the www-manage ability. MCP tools: WwwStatus, WwwAdd, WwwForceToRoot, WwwForceFromRoot, WwwClear.

POST /api/apps/{name}/ssl/force re-applies the HTTP → HTTPS redirect without issuing a new certificate (cipi ssl force). Requires ssl-manage and API 1.12.0+. MCP tool: SslForce.

GET /api/dbs lists databases synchronously by running sudo cipi db list on the host (same as the server CLI). Optional query engine=mariadb|pgsql filters by engine (API 1.12.0+ / Cipi 4.8+). GET /api/dbs/engines lists installed engines and the server default (sync; MCP DbEngines). Other /api/dbs/* write operations are asynchronous jobs and accept optional engine on create, delete, backup, restore, and password. Database commands require Cipi 4.4.17+ on the server (cipi db … entries in the API sudoers whitelist); multi-engine support needs Cipi 4.8+.

POST /api/apps/{name}/webhook/recreate recreates the GitHub/GitLab deploy webhook; optional body { "rotate_secret": true } also rotates CIPI_WEBHOOK_TOKEN in apps.json and shared/.env. Async job (app-webhook-recreate; ability apps-edit; CLI cipi app webhook recreate [--rotate-secret]; API 1.15.0+ / Cipi 5.0.6+). MCP: AppWebhookRecreate.

PHP management (API 1.15.0+ / Cipi 5.0.6+): GET /api/php lists installed versions (sync; ability php-view). POST /api/php/install and DELETE /api/php/{version} install or remove a version (async; php-manage). Installable versions are 8.3, 8.4, 8.5. MCP: PhpList.

DB enginesPOST /api/dbs/engines/install (ability dbs-manage; API 1.15.0+).

Removed in API 1.19.0: DELETE /api/php/{version}, PUT /api/php/default, DELETE /api/dbs/{name} (and MCP DbDelete) and PUT /api/dbs/engines/default. Removing PHP, switching the default PHP or engine, and dropping a database are destructive host operations — run cipi php remove|switch, cipi db delete and cipi db default on the server.

SSH keysGET|POST /api/ssh/keys, DELETE /api/ssh/keys/{n} (abilities ssh-view / ssh-manage; API 1.15.0+).

ServicesGET /api/services, POST /api/services/{name}/restart (abilities services-view / services-manage; API 1.15.0+).

SMTPGET|PUT|DELETE /api/smtp, POST /api/smtp/enable|disable|test (abilities smtp-view / smtp-manage; password never returned on GET; API 1.15.0+ / Cipi 5.0.6+ non-interactive cipi smtp configure --host=…).

HealthchecksGET /api/health, GET|PUT|DELETE /api/apps/{name}/health, POST /api/apps/{name}/health/check (abilities health-view / health-manage; API 1.15.0+).

IP whitelist — middleware cipi.ip on api/* and /mcp reads /etc/cipi/api-ip-whitelist (missing file or * = allow all). Rejected clients get 403 { "error": "IP not allowed", "ip": "…" }. REST: GET /api/ip-whitelist, PUT /api/ip-whitelist (entries, optional ensure_client_ip), POST /api/ip-whitelist (ip), DELETE /api/ip-whitelist (ip), POST /api/ip-whitelist/allow-all (abilities ip-whitelist-view / ip-whitelist-manage; CLI cipi api ip-whitelist; API 1.15.0+ / Cipi 5.0.6+). MCP: IpWhitelistShow.

GET /api/status returns the same structured JSON as cipi status (system, resources, services, PHP pools, app count). Since API 1.11.8+ the endpoint prefers sudo cipi status on the host and falls back to direct host reads when sudo is unavailable. Since API 1.12.1+ the host-read fallback includes postgresql when the systemd unit is installed (matching Cipi 4.8+). Requires the status-view ability (API 1.11.6+). The MCP tool ServerStatus returns the same payload and requires only mcp-access. From your laptop, use cipi-cli status for a global overview across all configured server profiles, or detail for one profile.

GET /api/apps/{name}/logs returns synchronous, paginated log snapshots for nginx, PHP-FPM, Laravel (when present), worker, and deploy logs — the REST counterpart to cipi app logs and cipi-cli apps logs. Query params: type (default all), page (default 1, most recent first), per_page (default 50, max 1000). Requires the apps-view ability and API package 1.11.9+. Log text is redacted for common secrets (same policy as MCP AppLogs since API 1.11.5+).

Redirects (API 1.31.0+) wrap cipi redirect synchronously: the CLI validates the rule, regenerates the vhost, runs nginx -t and reverts on failure. GET /api/apps/{name}/redirects returns the whole-app redirect and the path redirects (redirects-view). PUT /api/apps/{name}/redirect with { "to", "code"?, "keep_path"? } sends every hostname of the app to the target in one hop; POST …/redirect/enable and …/redirect/disable toggle it without forgetting the target; DELETE …/redirect removes it. POST /api/apps/{name}/redirects with { "from", "to", "code"?, "keep_path"? } adds or updates a path redirect (a from ending in / is a prefix), and DELETE with { "from" } removes it. Writes need redirects-manage. MCP: RedirectList, RedirectSet, RedirectToggle, RedirectUnset, RedirectAdd, RedirectRemove.

Proxies (API 1.31.0+) wrap cipi proxy: GET /api/apps/{name}/proxies (proxies-view), POST with { "prefix", "upstream", "strip_prefix"?, "preserve_host"?, "timeout"?, "buffering"? } and DELETE with { "prefix" } (proxies-manage). The loopback guard is never bypassed from the API — there is no --force — so upstreams on ports Cipi already uses (nginx, SSH, MariaDB, PostgreSQL, Valkey, Meilisearch, another app's Octane or Reverb) are refused. MCP: ProxyList, ProxyAdd, ProxyRemove.

Node apps (API 1.31.0+ / Cipi 5.4.0+): POST /api/apps accepts node (spa|static|ssr), framework (next|nuxt|sveltekit|astro|remix|vite), node_version, build, start, output and health_path — see Node apps. Node apps require a repository and refuse custom, octane, engine and php. PUT /api/apps/{name} accepts the same fields; node_version also pins a Laravel app's asset builds to a Node major (default follows the server again). GET /api/node lists installed runtimes, the server default and the apps on each major; GET /api/apps/{name}/node shows an app's Node status (node-view). POST /api/apps/{name}/node/restart is a blue/green restart, async job node-restart (node-manage). App list/show expose node, node_mode and node_version, plus redirect, redirects and proxies. Installing, upgrading, removing runtimes and changing the server default stay on the CLI (cipi node). MCP: NodeRuntimes, NodeStatus, NodeRestart.

Deploy audit (API 1.31.0+ / Cipi 5.4.0+) — GET /api/apps/{name}/deploy/audit?days=90 returns the app's records from the hash-chained deploy ledger (cipi deploy <app> --audit --json): event, release, commit, origin (cli, panel, webhook, ssh, cron, …), operator, IP and the app's claimed fields. The list is empty until the first deploy after the update. Ability deploy-manage; MCP DeployAudit.

Search (API 1.31.0+ / Cipi 5.2.2+) — GET /api/search (search-view) and POST /api/apps/{name}/search/enable|disable (search-manage, Laravel apps only) wrap cipi search. Installing, upgrading or removing Meilisearch and rotating its master key stay on the CLI. MCP: SearchStatus, SearchEnable, SearchDisable.

Host insights (API 1.31.0+, read-only by design) — GET /api/packages (packages-view, cipi package list), GET /api/monitor (monitor-view, checks with config, state and last alert) and GET /api/zt (zt-view, parsed cipi zt status plus raw output). MCP: PackageList, MonitorList, ZtStatus.

Fix permissions (API 1.31.0+ / Cipi 5.2.1+) — POST /api/apps/{name}/fix-permissions restores the app home permission model (cipi app fix-permissions). Async job app-fix-permissions; ability apps-edit; MCP AppFixPermissions. Since API 1.31.0, app create and edit also accept a wildcard primary domain (*.example.com, Cipi 5.1.1+).

Method Endpoint Required ability
GET /api/apps apps-view
GET /api/apps/{name} apps-view
GET /api/apps/{name}/logs apps-view
POST /api/apps apps-create
PUT /api/apps/{name} apps-edit
POST /api/apps/{name}/webhook/recreate apps-edit
POST /api/apps/{name}/fix-permissions apps-edit
POST /api/apps/{name}/suspend apps-suspend
POST /api/apps/{name}/unsuspend apps-suspend
DELETE /api/apps/{name} apps-delete
GET /api/apps/{name}/basicauth apps-basicauth
POST /api/apps/{name}/basicauth/enable apps-basicauth
POST /api/apps/{name}/basicauth/disable apps-basicauth
GET|PUT /api/apps/{name}/env apps-env
GET|POST|PUT|DELETE /api/apps/{name}/auth apps-auth
POST /api/apps/{name}/artisan apps-artisan
GET /api/run-commands apps-run
POST /api/apps/{name}/run apps-run
GET|PUT /api/apps/{name}/deploy-config apps-deploy-config
GET /api/apps/{name}/aliases aliases-view
POST /api/apps/{name}/aliases/{alias} aliases-create
DELETE /api/apps/{name}/aliases/{alias} aliases-delete
GET /api/apps/{name}/www www-manage
POST /api/apps/{name}/www/add www-manage
POST /api/apps/{name}/www/force-to-root www-manage
POST /api/apps/{name}/www/force-from-root www-manage
POST /api/apps/{name}/www/clear www-manage
GET /api/apps/{name}/redirects redirects-view
PUT|DELETE /api/apps/{name}/redirect redirects-manage
POST /api/apps/{name}/redirect/enable|disable redirects-manage
POST|DELETE /api/apps/{name}/redirects redirects-manage
GET /api/apps/{name}/proxies proxies-view
POST|DELETE /api/apps/{name}/proxies proxies-manage
GET /api/node node-view
GET /api/apps/{name}/node node-view
POST /api/apps/{name}/node/restart node-manage
POST /api/apps/{name}/deploy deploy-manage
POST /api/apps/{name}/deploy/rollback deploy-manage
POST /api/apps/{name}/deploy/unlock deploy-manage
GET /api/apps/{name}/deploy/audit deploy-manage
POST /api/apps/{name}/ssl ssl-manage
POST /api/apps/{name}/ssl/force ssl-manage
GET /api/dbs/engines dbs-view
POST /api/dbs/engines/install dbs-manage
GET /api/dbs dbs-view
POST /api/dbs dbs-create
POST /api/dbs/{name}/backup dbs-manage
POST /api/dbs/{name}/restore dbs-manage
POST /api/dbs/{name}/password dbs-manage
GET /api/php php-view
POST /api/php/install php-manage
GET /api/ssh/keys ssh-view
POST /api/ssh/keys ssh-manage
DELETE /api/ssh/keys/{n} ssh-manage
GET /api/services services-view
POST /api/services/{name}/restart services-manage
GET /api/smtp smtp-view
PUT /api/smtp smtp-manage
POST /api/smtp/enable|disable|test smtp-manage
DELETE /api/smtp smtp-manage
GET /api/health health-view
GET /api/apps/{name}/health health-view
PUT|DELETE /api/apps/{name}/health health-manage
POST /api/apps/{name}/health/check health-view
GET /api/search search-view
POST /api/apps/{name}/search/enable|disable search-manage
GET /api/packages packages-view
GET /api/monitor monitor-view
GET /api/zt zt-view
GET /api/status status-view
GET /api/jobs/{id} any authenticated token
GET /api/ip-whitelist ip-whitelist-view
PUT|POST|DELETE /api/ip-whitelist (+ /allow-all) ip-whitelist-manage

REST examples (curl)

Set your API base URL and token (from cipi api token create):

bash
export CIPI_API_URL="https://api.myserver.com"
export CIPI_API_TOKEN="your-sanctum-token"

List apps (sync, 200):

bash
curl -sS "${CIPI_API_URL}/api/apps" \
  -H "Authorization: Bearer ${CIPI_API_TOKEN}" \
  -H "Accept: application/json"

Server status (sync, requires status-view):

bash
curl -sS "${CIPI_API_URL}/api/status" \
  -H "Authorization: Bearer ${CIPI_API_TOKEN}"

App logs (sync, requires apps-view, API 1.11.9+):

bash
curl -sS "${CIPI_API_URL}/api/apps/myapp/logs?type=deploy&page=1&per_page=50" \
  -H "Authorization: Bearer ${CIPI_API_TOKEN}" \
  -H "Accept: application/json"

Create a Laravel Octane app (async, API 1.13.0+ / Cipi 5.0+; requires apps-create):

bash
curl -sS -X POST "${CIPI_API_URL}/api/apps" \
  -H "Authorization: Bearer ${CIPI_API_TOKEN}" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{
    "domain": "shop.example.com",
    "repository": "git@github.com:you/shop.git",
    "branch": "main",
    "octane": true,
    "engine": "mariadb"
  }'

Send "octane": "frankenphp" for the same effect. Omit octane for classic PHP-FPM. Use "engine": "pgsql" when PostgreSQL is installed (API 1.12.0+ / Cipi 4.8+).

App .env (sync, API 1.14.0+ / Cipi 5.0.3+; requires apps-env):

bash
curl -sS "${CIPI_API_URL}/api/apps/myapp/env" \
  -H "Authorization: Bearer ${CIPI_API_TOKEN}"

curl -sS -X PUT "${CIPI_API_URL}/api/apps/myapp/env" \
  -H "Authorization: Bearer ${CIPI_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"set":{"APP_DEBUG":"false"},"unset":["LEGACY_KEY"]}'

Artisan / app run (async jobs, API 1.14.0+; abilities apps-artisan / apps-run):

bash
curl -sS -X POST "${CIPI_API_URL}/api/apps/myapp/artisan" \
  -H "Authorization: Bearer ${CIPI_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"command":"cache:clear"}'

curl -sS -X POST "${CIPI_API_URL}/api/apps/myapp/run" \
  -H "Authorization: Bearer ${CIPI_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"command":"composer install --no-dev"}'

curl -sS "${CIPI_API_URL}/api/run-commands" \
  -H "Authorization: Bearer ${CIPI_API_TOKEN}"

Poll GET /api/jobs/{id} for output / exit_code. Job types: app-artisan, app-run.

Deploy an app (async, 202 + job_id):

bash
curl -sS -X POST "${CIPI_API_URL}/api/apps/myapp/deploy" \
  -H "Authorization: Bearer ${CIPI_API_TOKEN}" \
  -H "Accept: application/json"

# poll until completed
curl -sS "${CIPI_API_URL}/api/jobs/JOB_ID" \
  -H "Authorization: Bearer ${CIPI_API_TOKEN}"

Create a Node SSR app (async, API 1.31.0+ / Cipi 5.4.1+; requires apps-create):

bash
curl -sS -X POST "${CIPI_API_URL}/api/apps" \
  -H "Authorization: Bearer ${CIPI_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "domain": "web.example.com",
    "repository": "git@github.com:you/web.git",
    "node": "ssr",
    "framework": "next",
    "node_version": "22"
  }'

Redirects and proxies (sync, API 1.31.0+ / Cipi 5.4.1+; redirects-manage / proxies-manage):

bash
curl -sS -X POST "${CIPI_API_URL}/api/apps/myapp/redirects" \
  -H "Authorization: Bearer ${CIPI_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"from":"/blog/","to":"https://blog.example.com/","code":301}'

curl -sS -X POST "${CIPI_API_URL}/api/apps/myapp/proxies" \
  -H "Authorization: Bearer ${CIPI_API_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"prefix":"/api/","upstream":"http://127.0.0.1:3000","strip_prefix":true}'

Deploy audit (sync, API 1.31.0+ / Cipi 5.4.0+; deploy-manage):

bash
curl -sS "${CIPI_API_URL}/api/apps/myapp/deploy/audit?days=30" \
  -H "Authorization: Bearer ${CIPI_API_TOKEN}"

Database backup (async, dbs-manage) — returns a real backup path in the job result, not an anonymized dump (see Agent anonymizer):

bash
curl -sS -X POST "${CIPI_API_URL}/api/dbs/myapp_db/backup" \
  -H "Authorization: Bearer ${CIPI_API_TOKEN}"

Host integration (sudoers)

The panel API runs as www-data and executes Cipi CLI commands via sudo using /etc/sudoers.d/cipi-api — an explicit whitelist of cipi subcommands. Vault and MariaDB credentials stay inside Cipi, not in PHP.

  • GET /api/dbs — runs sudo cipi db list (sync). Requires Cipi 4.4.17+ (migration adds cipi db … to sudoers). Without it: sudo: a terminal is required. Multi-engine list/engines need Cipi 4.8+ / API 1.12.0+.
  • GET /api/status / MCP ServerStatus — prefer sudo cipi status (API 1.11.8+); host-read fallback when sudo fails (includes postgresql since API 1.12.1+).
  • MCP ServiceListsudo cipi service list
  • MCP AppArtisansudo cipi app artisan <app> …
  • Since Cipi 5.0.6+ / API 1.15+: php list|install|remove|switch, ssh list|add|remove, service list|restart, status, db install|default|engines, app webhook recreate, smtp status|configure|enable|disable|test|delete, api ip-whitelist (+ args) in the sudoers whitelist (migration 5.0.6 creates the default IP whitelist file and regenerates /etc/sudoers.d/cipi-api on cipi self-update).
  • Since Cipi 5.0.3+ / API 1.14+: app env, app artisan, app run, auth create|edit|show|delete, and deploy-config in the sudoers whitelist (migration regenerates /etc/sudoers.d/cipi-api on cipi self-update).
  • Since Cipi 5.2.2+: search status|list|enable|disable and package list in the sudoers whitelist. search install|upgrade|key rotate|remove and package install|remove stay off it — they change or destroy server state and stay with the operator on the CLI.
  • Since Cipi 5.2.3+: the manual stack upgradesnginx upgrade, db upgrade and service upgrade — are not in the sudoers whitelist either, for the same reason as package install: restarting a database engine is an operator decision, taken at the CLI while you are watching.
  • Since Cipi 5.3.0+: zt status and monitor list in the sudoers whitelist. zt enable, lock and the rest, and monitor enable|disable|set, stay with the operator on the CLI — the same rule as package install.
  • Since Cipi 5.4.1+ (API 1.31.0+): redirect set|enable|disable|unset|add|remove|list, proxy add|remove|list and node list|status|restart — each rule the usual exact command line. deploy <app> --audit reads the root-only ledger. Migration 5.4.1 regenerates /etc/sudoers.d/cipi-api and checks it with visudo -cf. What deliberately stays CLI-root: Node runtime management (node install|default|upgrade|remove), search install|upgrade|key-rotate, package install|remove and every zt mutation — installing software on the server is not something a web panel should be able to do, compromised or not. The redirect/proxy validation is the CLI's own, so the panel gets exactly the same refusals.
  • Async jobs — cipi app (including --octane / --engine), deploy, alias, www, ssl / ssl force, db create|delete|backup|restore|password|engines, etc.

After cipi self-update, run cipi api fix-permissions if /docs or /mcp return HTTP 500 (see troubleshooting above).

IP whitelist (CLI)

Since Cipi 5.0.6+, restrict panel API and MCP clients by source IP. Default file /etc/cipi/api-ip-whitelist is * (allow all). One IPv4/IPv6 address or CIDR per line (or comma-separated --ips=).

bash
$ cipi api ip-whitelist show
$ cipi api ip-whitelist add 203.0.113.10
$ cipi api ip-whitelist set --ips=203.0.113.0/24,2001:db8::/32
$ cipi api ip-whitelist allow-all
$ cipi api ip-whitelist show --json

REST equivalents live under /api/ip-whitelist (API 1.15.0+). PUT auto-adds the caller IP when tightening the list unless ensure_client_ip: false.

Swagger / OpenAPI

Interactive documentation is available at /docs (Swagger UI). The OpenAPI spec is generated from public/api-docs/openapi.json and covers apps (including suspend, unsuspend, domain rename, basic auth, .env, Composer auth.json, Artisan / app-run jobs, deploy-config, fix-permissions, paginated logs, www redirects, Octane and Node create, and multi-engine engine), app and path redirects, proxies, Node runtimes, aliases, deploy and deploy audit, SSL (install + force HTTPS), databases (engines list + optional engine on mutations), search, packages, monitor, Zero Trust, server status, job polling with structured result types, and MCP tool schemas. Current API package version: 1.31.0 (1.30.0 was published in error from an old commit — skip it).

MCP server

An MCP (Model Context Protocol) server is exposed at /mcp via Streamable HTTP. Since API package 1.11.1+, a token with the mcp-access ability is sufficient for all MCP tools — per-endpoint REST abilities (apps-view, deploy-manage, apps-basicauth, www-manage, etc.) are not checked on /mcp. The server exposes 68 tools (one tools/list page) for app, alias, www, redirects, proxies, Node, database, deploy and deploy audit, SSL, HTTP Basic Auth, search, server management (PHP, SSH, services, SMTP, health, IP whitelist, packages, monitor, Zero Trust), .env / auth.json / app-run / deploy-config, job polling, logs, Artisan, and server monitoring. Write operations that dispatch async jobs return a job_id — poll with JobShow (API 1.11.0+). Basic auth actions and read-only tools run synchronously.

  • Applications: AppList, AppShow, AppCreate (optional engine, octane), AppEdit, AppSuspend, AppUnsuspend, AppDelete, AppDeploy, AppDeployRollback, AppDeployUnlock, AppArtisan (Laravel apps only; rejects custom apps and tinker), AppEnvShow, AppEnvUpdate, AppAuthJson*, AppRun, AppRunCommands, AppDeployConfigShow, AppDeployConfigUpdate, AppWebhookRecreate (API 1.15.0+ / Cipi 5.0.6+; earlier app tooling requires API 1.14.0+ / Cipi 5.0.3+), AppFixPermissions (API 1.31.0+)
  • Redirects & proxies: RedirectList, RedirectSet, RedirectToggle, RedirectUnset, RedirectAdd, RedirectRemove, ProxyList, ProxyAdd, ProxyRemove (API 1.31.0+ / Cipi 5.4.1+)
  • Node: NodeRuntimes, NodeStatus, NodeRestart (API 1.31.0+ / Cipi 5.4.1+)
  • Deploy audit: DeployAudit (API 1.31.0+ / Cipi 5.4.0+)
  • Search: SearchStatus, SearchEnable, SearchDisable (API 1.31.0+ / Cipi 5.2.2+)
  • Server management: PhpList, IpWhitelistShow (API 1.15.0+ / Cipi 5.0.6+); PackageList, MonitorList, ZtStatus — read-only (API 1.31.0+)
  • HTTP Basic Auth: AppBasicAuthStatus, AppBasicAuthEnable, AppBasicAuthDisable
  • Aliases: AliasList, AliasAdd, AliasRemove
  • WWW / apex: WwwStatus, WwwAdd, WwwForceToRoot, WwwForceFromRoot, WwwClear (API 1.12.0+)
  • Databases: DbEngines, DbList, DbCreate, DbBackup, DbRestore, DbPassword (optional engine on list/mutations; API 1.12.0+; DbDelete removed in 1.19.0)
  • SSL: SslInstall, SslForce (API 1.12.0+)
  • Jobs & logs: JobShow (poll async job status, parsed result, and CLI output), AppLogs (recent app logs by type: all, nginx, php, worker, deploy, laravel — same as cipi app logs; REST equivalent: GET /api/apps/{name}/logs since API 1.11.9+), ApiLogShow (recent Laravel logs for the panel API host)
  • Server monitoring: ServerStatus (structured JSON matching GET /api/status / cipi status), ServiceList (system service status via cipi service list)
Since API 1.11.5+, MCP log tools (AppLogs, ApiLogShow) prefix every response with a production-content warning and redact common secrets before delivery. Sensitive CLI output from JobShow and AppArtisan is redacted too; structured job result objects (e.g. app credentials from create jobs) are left intact so operators can still read them once.

Since Cipi 4.6.3, the panel API package is soft-updated nightly at 04:30 via /etc/cron.d/cipi-api (cipi api update), so MCP and REST endpoints stay current without manual intervention.

Installing the MCP server

The MCP endpoint is optional and loads only when the required MCP package is installed. To use it from VS Code, Cursor, or Claude Desktop:

  1. Configure the API with cipi api <domain> and cipi api ssl
  2. Create a token with cipi api token create and select at least mcp-access
  3. Add the MCP server to your client config (see below)

Cursor

Add to ~/.cursor/mcp.json (or Cursor → Settings → MCP):

json
{
  "mcpServers": {
    "cipi-api": {
      "type": "http",
      "url": "https://<your-api-domain>/mcp",
      "headers": {
        "Authorization": "Bearer <your-token>"
      }
    }
  }
}

Cursor connects natively over HTTP — no bridge needed.

VS Code

VS Code (with GitHub Copilot) supports MCP natively since 1.102. Add to .vscode/mcp.json or run MCP: Open User Configuration for a global setup. Use inputs to prompt for the token once and store it securely:

json
{
  "inputs": [
    {
      "type": "promptString",
      "id": "cipi-token",
      "description": "Cipi API Token",
      "password": true
    }
  ],
  "servers": {
    "cipi-api": {
      "type": "http",
      "url": "https://<your-api-domain>/mcp",
      "headers": {
        "Authorization": "Bearer ${input:cipi-token}"
      }
    }
  }
}

Restart VS Code after saving. Use MCP: Add Server from the Command Palette for a guided setup.

Claude Code

Add the MCP server directly from the CLI:

bash
$ claude mcp add --transport http cipi-api https://<your-api-domain>/mcp \
    --header "Authorization: Bearer <your-token>"

Claude Desktop

Claude Desktop requires the mcp-remote bridge to convert stdio to HTTP. Add to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or the equivalent config path on your OS:

json
{
  "mcpServers": {
    "cipi-api": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://<your-api-domain>/mcp",
        "--header",
        "Authorization: Bearer <your-token>"
      ]
    }
  }
}

Install mcp-remote once with npm install -g mcp-remote.

Replace <your-api-domain> with your API domain (e.g. api.myhosting.com) and <your-token> with the token created in step 2.

WHMCS Module

An official WHMCS provisioning module is available at github.com/cipi-sh/whmcs. It bridges the WHMCS provisioning lifecycle to the Cipi REST API — automating app creation, deletion, SSL certificates, deployments, and configuration changes for your hosting customers. No Composer dependencies; the module is a self-contained drop-in.

Requirements

  • WHMCS 8.x (provisioning module type “Server”)
  • Cipi server with API enabled: cipi api <domain> and cipi api ssl
  • Sanctum Bearer token with the required abilities:
Ability Required for
apps-view Test Connection, App Info
apps-create Create Account
apps-edit Change Package
apps-suspend Suspend / Unsuspend
apps-delete Terminate Account
deploy-manage Deploy, Rollback, Unlock
ssl-manage Install SSL, Auto-SSL

Installation

  1. Copy modules/servers/cipi/ into your WHMCS root:
    your-whmcs/
    └── modules/
        └── servers/
            └── cipi/
                ├── cipi.php
                └── lib/
                    └── CipiApiClient.php
  2. In WHMCS Admin → System Settings → Servers → Add New Server:
    • Type: Cipi (Laravel hosting)
    • Hostname: API base URL (e.g. https://api.example.com, no trailing slash)
    • Password: Bearer token from cipi api token create
    • Secure: Yes (recommended — enables TLS verification)
  3. Create a hosting product linked to this server and configure Module Settings:
Setting Description Default
PHP Version 8.2 / 8.3 / 8.4 / 8.5 8.5
App Type laravel or custom laravel
Git Repository (SSH) Required for Laravel; optional for custom
Git Branch Branch to deploy main
Auto SSL Install Let’s Encrypt after creation No

App types

App type Cipi equivalent Stack Git / Deploy
laravel (default) cipi app create Isolated Linux user, PHP-FPM pool, Nginx vhost, MariaDB, Supervisor workers, Deployer releases SSH repository URL required; branch from settings
custom cipi app create --custom htdocs/ directory, Nginx + PHP — ideal for static sites, SPAs, WordPress, or generic PHP apps Optional: leave Git Repository empty for SFTP-only hosting, or set a repo for Git-based deploy

Provisioning lifecycle

WHMCS action API call Behaviour
Test Connection GET /api/apps Validates token and API reachability
Create Account POST /api/apps Provisions a Cipi app (Laravel or custom); waits for async jobs; optionally installs SSL
Suspend POST /api/apps/{name}/suspend Takes the app offline (HTTP 503 maintenance page) without deleting it; waits for async jobs
Unsuspend POST /api/apps/{name}/unsuspend Restores the app’s normal Nginx vhost; waits for async jobs
Terminate Account DELETE /api/apps/{name} Removes the app; waits for async jobs
Change Package PUT /api/apps/{name} Updates PHP version, Git repository, or branch

Suspend / Unsuspend require Cipi 4.5.8+ (suspend/unsuspend endpoints), the API package 1.8.1+, and a token with the apps-suspend ability. Suspending swaps the app’s vhost for a generic HTTP 503 maintenance page; unsuspending restores it.

Admin buttons

From the WHMCS admin service view, operators can trigger one-click actions:

Button API call Description
Install SSL POST /api/apps/{name}/ssl Install a Let’s Encrypt certificate
Deploy POST /api/apps/{name}/deploy Trigger a zero-downtime deployment
Rollback Deploy POST /api/apps/{name}/deploy/rollback Revert to the previous release
Unlock Deploy POST /api/apps/{name}/deploy/unlock Unlock a stuck deployment
App Info GET /api/apps/{name} Fetch current app details into the Module Log

Auto-SSL on creation

Enable Auto SSL in the product module settings to automatically install a Let’s Encrypt certificate right after provisioning. If SSL installation fails, the app is still created successfully and a warning is logged.

Full API client

The bundled CipiApiClient covers the entire Cipi REST API surface. Even if a feature is not wired to a WHMCS hook, you can use the client in custom hooks or addons:

Area Methods
Apps listApps, getApp, createApp, editApp, suspendApp, unsuspendApp, deleteApp
Deploy deployApp, rollbackDeploy, unlockDeploy
SSL installSsl
Aliases listAliases, addAlias, removeAlias
Databases listDatabases, createDatabase, deleteDatabase, backupDatabase, restoreDatabase, resetDatabasePassword
Jobs getJob, waitForJob

Extending the module

php
// Example: add an alias from a WHMCS hook
require_once ROOTDIR . '/modules/servers/cipi/lib/CipiApiClient.php';

$client = new CipiApiClient('https://api.example.com', $token);
$client->addAlias('myapp', 'alias.example.com');

// Example: create an extra database
$client->createDatabase('myapp_extra');

// Example: backup a database
$client->backupDatabase('myapp');

Customer-facing behaviour

The module does not add a Client Area tab, custom buttons, or live status from Cipi. Customers see the standard WHMCS service view (domain, status, renewal dates). When Cipi provisions an app it generates one-time secrets (SSH password, DB password, deploy key, webhook URL). The REST API does not automatically push those secrets into WHMCS — you should extend the module, write a hook, or deliver credentials through your support workflow.

Module logging

All API calls are logged via logModuleCall() — Create, Terminate, Change Package, SSL, Deploy, Rollback, Unlock, and App Info. Enable Utilities → Logs → Module Log in WHMCS Admin for full visibility.

Full source code, project structure, and contribution guidelines are available on GitHub. The module is open-source under the MIT License.

cipi sync

Transfer, replicate, and back up entire Laravel applications between Cipi servers — including configuration, database dumps, storage files, SSH keys, workers, and crontabs. Every archive is encrypted with AES-256-CBC and protected by a user-defined passphrase, so credentials and sensitive data are safe at rest and during transfer.

Commands overview

bash
$ cipi sync export  [app ...] [--with-db] [--with-storage] [--output=<path>] [--passphrase=<secret>]
$ cipi sync import  <archive.tar.gz.enc> [app ...] [--update] [--deploy] [--yes] [--passphrase=<secret>]
$ cipi sync push    [app ...] [--host=IP] [--port=22] [--with-db] [--with-storage] [--import] [--passphrase=<secret>]
$ cipi sync list    <archive.tar.gz.enc> [--passphrase=<secret>]
$ cipi sync pubkey  # display the server's sync public key for inter-server trust
$ cipi sync trust   # add a remote server's public key to cipi's authorized_keys

Archive encryption

All sync archives are encrypted by default with AES-256-CBC. During export you are prompted for a passphrase (minimum 8 characters) that protects the archive. The same passphrase is required to import or inspect it. This protects SSH keys, .env files, database dumps, and credentials at rest and during transfer.

bash
# Interactive mode (default) — prompted for passphrase
$ cipi sync export --with-db
#   Enter passphrase to encrypt the archive: ********
#   Confirm passphrase: ********

# Non-interactive mode — for cron jobs and scripts
$ cipi sync export --with-db --passphrase="MyStr0ngP@ss"
For automated setups, store the passphrase in a secure file and reference it in your scripts: echo "MyStr0ngP@ss" > /etc/cipi/.sync_passphrase && chmod 400 /etc/cipi/.sync_passphrase. Then use --passphrase="$(cat /etc/cipi/.sync_passphrase)" in cron jobs.

Export

Packs app configs into an encrypted .tar.gz.enc archive. Optionally includes database dumps and storage files.

bash
# Export all apps (config only)
$ cipi sync export

# Export three specific apps with database + storage
$ cipi sync export shop blog api --with-db --with-storage

# Export to a custom path (non-interactive)
$ cipi sync export --with-db --output=/root/backups/cipi-march.tar.gz --passphrase="MyStr0ngP@ss"

What goes into the archive

File Description Included
env The app's .env from /home/<app>/shared/.env Always
auth.json Composer auth credentials (if exists) Always
deploy.php Deployer config Always
ssh/* Deploy key, known_hosts, authorized_keys, SSH config Always
supervisor.conf Queue workers config Always
crontab App's crontab (scheduler + deploy trigger) Always
db.sql.gz Gzipped MariaDB dump (schema + data + routines) --with-db
storage.tar.gz Archive of /home/<app>/shared/storage/ --with-storage

Plus global configs: apps.json (filtered to selected apps), databases.json, backup.json, api.json.

Import

Restores apps from an archive onto the current server.

bash
# Import all apps from archive
$ cipi sync import /tmp/cipi-sync-aws01-20260306.tar.gz.enc

# Import only two apps from an archive that contains ten
$ cipi sync import /tmp/cipi-sync-aws01-20260306.tar.gz.enc shop blog --passphrase="MyStr0ngP@ss"

# Import and deploy code from Git immediately
$ cipi sync import /tmp/cipi-sync-aws01-20260306.tar.gz.enc --deploy

# Non-interactive (skip all prompts)
$ cipi sync import /tmp/cipi-sync-aws01-20260306.tar.gz.enc --yes --passphrase="MyStr0ngP@ss"

What import does for a NEW app

When an app does not exist on the target server, import creates it from scratch — equivalent to cipi app create with all configs pre-filled from the archive:

  1. Linux user — Creates a new user with a random password
  2. Directories — Creates /home/<app>/shared/, logs/, .ssh/, .deployer/
  3. SSH deploy key — Restores from archive (same key works with GitHub/GitLab without reconfiguration)
  4. MariaDB database — Creates database + user with a new random password
  5. Database data — Imports the dump if --with-db was used during export
  6. .env — Copies from archive, then overwrites DB_PASSWORD, DB_USERNAME, DB_DATABASE, DB_HOST with the new server's values. Everything else (APP_KEY, MAIL_*, REDIS_*, custom vars) stays as-is
  7. PHP-FPM pool, Nginx vhost, Supervisor workers, Crontab, Deployer — Fully configured from archive data
At the end of import, Cipi prints the new SSH and DB passwords. Save them — they are shown only once.

Safety checks before import

The import runs pre-flight checks before touching anything:

  • App already exists — blocked unless --update is passed
  • Domain conflict — blocked if another app already uses the same domain
  • Missing PHP version — warning (the app is skipped; install the version first with cipi php install)

Update mode (--update)

The key feature for repeated sync (e.g. failover replication). Without --update, import refuses to touch apps that already exist. With --update, it updates existing apps and creates new ones.

bash
$ cipi sync import /tmp/archive.tar.gz.enc --update --passphrase="MyStr0ngP@ss"

What update does for an existing app

  • .env sync — The archive .env replaces the local one, but DB_PASSWORD, DB_USERNAME, DB_DATABASE, and DB_HOST are preserved from the local server. Everything else (APP_KEY, MAIL_*, REDIS_*, custom vars) comes from the source.
  • Database data — If the archive has a dump, drops all tables (with SET FOREIGN_KEY_CHECKS=0) and reimports. Uses local root credentials.
  • Storage — If the archive has storage, extracts over the existing directory (new files added, existing overwritten).
  • PHP version migration — If the source uses a different PHP version, the update migrates FPM pool, supervisor, crontab, deployer, and .env automatically.
  • Nginx vhost, Supervisor workers, Deployer config — Regenerated from archive data.
  • Deploy — If --deploy is passed, runs dep deploy to pull latest code.

What update does NOT change

  • Linux user password
  • SSH deploy keys (kept from first import)
  • MariaDB user credentials (target keeps its own)
  • SSL certificates (run cipi ssl install separately)

List (inspect archive)

View what is inside an archive without importing anything.

bash
$ cipi sync list /tmp/cipi-sync-aws01-20260306.tar.gz.enc --passphrase="MyStr0ngP@ss"

Cipi Sync Archive
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  Cipi           v5.1.0
  Exported       2026-03-06T15:00:00Z
  Source         aws01 (3.120.xx.xx)
  Database       true
  Storage        true
 
  Apps
  APP            DOMAIN                       PHP    DB       STORAGE
  shop           shop.example.com             8.4    yes      yes
  blog           blog.example.com             8.4    yes      yes
  api            api.example.com              8.5    yes      yes
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Push (export + transfer + import)

Combines export, rsync transfer, and remote import in one command. Runs entirely from the source server.

bash
# Interactive push — prompted for target IP and passphrase
$ cipi sync push --with-db --with-storage --import

# Non-interactive push (for cron and scripts)
$ cipi sync push --host=51.195.xx.xx --port=22 --with-db --with-storage --import --passphrase="MyStr0ngP@ss"

# Push specific apps only
$ cipi sync push shop blog --host=51.195.xx.xx --with-db --import --passphrase="MyStr0ngP@ss"

# Push without auto-import (transfer only — import manually on remote)
$ cipi sync push --host=51.195.xx.xx --with-db --passphrase="MyStr0ngP@ss"

How push works

  1. Step 1: Runs cipi sync export locally (encrypts with passphrase)
  2. Step 2: Transfers the encrypted archive to the target via rsync
  3. Step 3: If --import is passed, runs cipi sync import --update --yes on the target via SSH

Push always adds --update and --yes when calling import on the remote. This means: first run creates all apps, subsequent runs incrementally update them. This is what makes push safe to run repeatedly via cron.

SSH setup for push

The source server needs SSH access to the target as the cipi user. Use the built-in trust mechanism for passwordless, key-based authentication between Cipi servers:

bash
# On the SOURCE server — display its sync public key
$ cipi sync pubkey

# On the TARGET server — add the source's public key to cipi's authorized_keys
$ cipi sync trust

Once trusted, cipi sync push connects as the cipi user automatically — no root access required.

Practical scenarios

Scenario 1: Migrate all apps from AWS to OVH

You have 20 apps on AWS. You bought an OVH VPS and installed Cipi on it.

bash
# On AWS (source server)
$ cipi sync push --host=51.195.xx.xx --with-db --with-storage --import

On the OVH target: 20 Linux users, 20 databases, 20 nginx vhosts, PHP-FPM pools, supervisor configs, crontabs — all created automatically. DB data imported, storage extracted, .env files copied with OVH's DB passwords, SSH deploy keys preserved (same keys work with GitHub). After import, install SSL and update DNS:

bash
# On OVH (target server)
$ cipi ssl install shop
$ cipi ssl install blog
# ... then update DNS A records to OVH IP

Scenario 2: Scheduled failover replication (cron)

Every 6 hours, Server 1 syncs all apps to Server 2. If Server 1 dies, change DNS and go live on Server 2.

bash
# One-time setup on Server 1 — trust Server 2 using cipi sync trust
$ cipi sync pubkey  # copy this key, then run "cipi sync trust" on Server 2
$ echo "YourStr0ngPassphrase!" > /etc/cipi/.sync_passphrase
$ chmod 400 /etc/cipi/.sync_passphrase

# First push (manual, to verify)
$ cipi sync push --host=server2-ip --with-db --with-storage --import --passphrase="$(cat /etc/cipi/.sync_passphrase)"

# Add to crontab for automatic replication
$ crontab -e
cron
0 */6 * * * /usr/local/bin/cipi sync push --host=51.195.xx.xx --port=22 --with-db --with-storage --import --passphrase="$(cat /etc/cipi/.sync_passphrase)" >> /var/log/cipi/sync-replica.log 2>&1

Data loss window equals the cron interval (6 hours in this example). When Server 1 goes down: change DNS to Server 2, run cipi ssl install for each app, and you are live.

Scenario 3: Replicate to multiple servers

cron
# Stagger by 30 minutes so exports don't run simultaneously
0 */6 * * * /usr/local/bin/cipi sync push --host=51.195.xx.xx --with-db --with-storage --import --passphrase="$(cat /etc/cipi/.sync_passphrase)" >> /var/log/cipi/sync-ovh.log 2>&1
30 */6 * * * /usr/local/bin/cipi sync push --host=164.90.xx.xx --with-db --with-storage --import --passphrase="$(cat /etc/cipi/.sync_passphrase)" >> /var/log/cipi/sync-do.log 2>&1

Scenario 4: Daily encrypted backup (no transfer)

cron
0 3 * * * /usr/local/bin/cipi sync export --with-db --with-storage --output=/root/backups/cipi-$(date +\%Y\%m\%d).tar.gz --passphrase="$(cat /etc/cipi/.sync_passphrase)" >> /var/log/cipi/export.log 2>&1

Creates an encrypted portable archive every night. Restore on any Cipi server at any time with cipi sync import.

Limitations

  • SSL certificates are not included in the archive. Run cipi ssl install after importing on a new server.
  • DB sync is full replace, not incremental. Each update drops all tables and reimports.
  • Storage sync is full extract, not incremental rsync. Deleted files on the source remain on the target.
  • Deploy keys are the same on source and target — no GitHub/GitLab reconfiguration needed.

Vault & Encryption

Cipi encrypts all configuration files at rest using AES-256-CBC. The Vault system provides transparent encryption and decryption so that sensitive data — database passwords, API tokens, SSH keys, .env contents — is never stored in plaintext on disk.

Architecture

The system is built on two layers:

  • Vault — transparent encryption of JSON configuration files on disk (server.json, apps.json, databases.json, backup.json, smtp.json, api.json)
  • Sync encryption — passphrase-based encryption of export archives for secure transfer between servers

How Vault works

A master key is generated during installation with openssl rand -base64 32 and stored at /etc/cipi/.vault_key (chmod 400, root-only). Every JSON config file is encrypted on disk with openssl enc -aes-256-cbc -salt -pbkdf2. Files keep the .json extension — the content is simply an encrypted blob instead of readable JSON.

The vault_read function auto-detects whether a file is plaintext or encrypted (backward compatibility), so existing servers migrate seamlessly during update.

Vault functions

bash
# Core functions in lib/vault.sh
vault_init          # Generate .vault_key if not present
vault_read <file>   # Decrypt and output JSON to stdout (auto-detect plain/encrypted)
vault_write <file>  # Read JSON from stdin, encrypt and write to disk
vault_seal <file>   # Encrypt an existing plaintext file in-place
vault_get <file> <jq_query>  # Shortcut: vault_read | jq

Public projection

Cipi generates an apps-public.json file containing only non-sensitive fields (domain, aliases, PHP version, branch, repository, user, creation timestamp, plus suspended and basic_auth flags). The cipi-api group reads this plaintext projection instead of the encrypted file, keeping the vault key restricted to root.

Sync archive encryption

When you run cipi sync export, configs are decrypted from the vault into a staging area, then the entire archive is encrypted with your passphrase. On import, the archive is decrypted with the passphrase, and configs are re-encrypted with the destination server's vault key.

If the vault key is lost, configuration files become irrecoverable. The key is protected by chmod 400 and included in server backups. Consider exporting it manually for additional safety.

Email Notifications

Cipi can send email alerts when backup errors, deploy failures, system cron job failures, or security-relevant authentication events occur. SMTP configuration is stored encrypted in /etc/cipi/smtp.json and included in sync exports. Since v5.3.0 the same notifications can also go to Slack, Discord, Telegram, ntfy or a webhook — see chat alert channels.

Commands

bash
$ cipi smtp configure      # interactive setup (Gmail, SendGrid, Mailgun, custom)
$ cipi smtp status          # display current notification settings
$ cipi smtp test            # send a verification email
$ cipi smtp enable          # enable notifications
$ cipi smtp disable         # disable without losing settings
$ cipi smtp delete          # remove SMTP configuration entirely

# Non-interactive (v5.0.6+) — panel API, scripts, automation
$ cipi smtp configure --host=smtp.example.com --port=587 --user=… --password=… \
    --from=alerts@example.com --to=ops@example.com --tls=on
$ cipi smtp status --json
$ cipi smtp delete --force

Granular notification triggers

Since v4.6.3, you can control which events send email when SMTP is configured. All triggers are on by default; events are always logged to /var/log/cipi/events.log regardless.

bash
$ cipi notifications list              # all triggers grouped by category
$ cipi notifications enable <trigger>   # turn one trigger on
$ cipi notifications disable <trigger>  # turn one trigger off
$ cipi notifications enable-all          # re-enable everything
$ cipi notifications disable-all         # mute all email alerts
$ cipi notifications reset               # restore defaults (all on)

Config: /etc/cipi/notifications.json. Run cipi notifications list on the server for the live on/off state. Trigger IDs for cipi notifications enable|disable <trigger>:

v5.1.0 adds five: backup_stale (a profile has not succeeded within twice its own interval), ini_set (a PHP setting changed), yml_apply and yml_fail (a project's cipi.yml was applied, or was invalid), and self_update (Cipi updated itself — so an unattended nightly update no longer lands without a word).

v5.2.2 adds eight more under two new categories: Search (search_install, search_enable, search_disable, search_key_rotate, search_upgrade, search_remove) and Packages (package_install, package_remove).

v5.2.3 adds five: yml_post_fail (a deploy.post step failed) and one per manually upgraded service — nginx_upgrade, mariadb_upgrade, pgsql_upgrade, valkey_upgrade.

v5.3.0 adds twelve. Eight under Monitor, one per cipi monitor check — monitor_disk, monitor_ssl, monitor_services, monitor_workers, monitor_http_5xx, monitor_fs, monitor_load — plus the recovery message monitor_ok. Four under Security for Cloudflare Zero Trust: zt_enable, zt_disable, zt_lock_http and zt_lock_ssh. Every trigger also reaches the chat channels you configure.

v5.3.1 adds two under Domains, both on by default: redirect_change for cipi redirect and proxy_change for cipi proxy.

v5.4.0 adds node_default under a new Node category: the server-wide Node version changed (cipi node default, or the 5.4.0 migration moving a server from Node 20 to 22). Since 5.4.0, api_update fires only when cipi api update actually changed a locked package, and names the old and new cipi/api versions — before, the 04:30 soft update sent it every night. A backup whose retention could not list or delete on S3 now sends backup_fail as well.

Fixed in v5.3.0: jq's // true treats false the same as “absent”, so an explicitly disabled flag read back as enabled. cipi notifications disable <trigger> wrote false, yet the trigger kept sending email and cipi notifications list showed it as on. And cipi smtp configure --tls=off stored tls: false, then generated /etc/msmtprc with tls on — which breaks a plain relay — while cipi smtp status reported the wrong value. An explicit false now stays false. If you rely on --tls=off, run cipi smtp configure again after updating so /etc/msmtprc is rewritten.
Trigger ID Category Event
app_createAppsApp created
app_editAppsApp modified
app_deleteAppsApp deleted
app_suspendAppsApp suspended
app_unsuspendAppsApp unsuspended
app_ssh_password_resetAppsApp SSH password reset
app_db_password_resetAppsApp DB password reset
alias_addDomainsAlias added
alias_removeDomainsAlias removed
www_addDomainsWWW alias added
www_force_to_rootDomainsWWW force to-root
www_force_from_rootDomainsWWW force from-root
www_clearDomainsWWW redirect cleared
redirect_changeDomainsApp or path redirect changed (5.3.1)
proxy_changeDomainsProxy prefix added or removed (5.3.1)
node_defaultNodeServer-wide Node version changed (5.4.0)
auth_createAuthComposer auth.json created
auth_editAuthComposer auth.json edited
auth_deleteAuthComposer auth.json deleted
basicauth_enableBasic authHTTP basic auth enabled
basicauth_disableBasic authHTTP basic auth disabled
deploy_successDeployDeploy succeeded
deploy_failDeployDeploy failed
deploy_rollbackDeployDeploy rollback
deploy_snapshot_failDeployPre-deploy DB snapshot failed
health_failHealthHTTP healthcheck failed (periodic, after 3 failures)
deploy_health_failHealthPost-deploy healthcheck failed
monitor_diskMonitorDisk usage over threshold (5.3.0)
monitor_sslMonitorSSL certificate expiring (5.3.0)
monitor_servicesMonitorSystem service not running (5.3.0)
monitor_workersMonitorQueue worker / Horizon not running (5.3.0)
monitor_http_5xxMonitorHTTP 5xx spike in access logs (5.3.0)
monitor_fsMonitorFilesystem read-only / config not writable (5.3.0)
monitor_loadMonitorLoad average over threshold (5.3.0)
monitor_okMonitorMonitor check recovered (5.3.0)
ssl_installSSLSSL certificate installed
ssl_forceSSLHTTP → HTTPS redirect forced
ssl_renewSSLSSL certificates renewed
php_installPHPPHP version installed
php_switchPHPSystem PHP switched
php_removePHPPHP version removed
php_upgradePHPPHP packages upgraded
nginx_upgradeStacknginx packages upgraded (5.2.3)
mariadb_upgradeStackMariaDB packages upgraded (5.2.3)
pgsql_upgradeStackPostgreSQL packages upgraded (5.2.3)
valkey_upgradeStackValkey packages upgraded (5.2.3)
db_createDatabaseDatabase created
db_deleteDatabaseDatabase deleted
worker_addWorkersWorker added
worker_removeWorkersWorker removed
ssh_key_addSSH keysSSH key added
ssh_key_renameSSH keysSSH key renamed
ssh_key_removeSSH keysSSH key removed
ssh_loginSecuritySSH login (cipi/root/sudo users)
sudoSecuritySudo elevation
suSecuritysu to root by cipi
zt_enableSecurityCloudflare Zero Trust enabled (5.3.0)
zt_disableSecurityCloudflare Zero Trust disabled (5.3.0)
zt_lock_httpSecurityHTTP ports locked (Cloudflare) (5.3.0)
zt_lock_sshSecuritySSH port closed (Cloudflare Tunnel) (5.3.0)
backup_failBackupBackup failed
backup_staleBackupBackup overdue (no successful run in its window) (5.1.0)
cron_failCronCron job failed
ini_setPHPPHP setting changed (5.1.0)
yml_applycipi.ymlcipi.yml applied (5.1.0)
yml_failcipi.ymlcipi.yml invalid or could not be applied (5.1.0)
yml_post_failcipi.ymlA deploy.post step failed (5.2.3)
self_updateUpdatesCipi updated itself (5.1.0)
reset_root_passwordResetRoot SSH password reset
reset_db_passwordResetMariaDB root password reset
reset_valkey_passwordResetValkey password reset
api_configureAPIPanel API configured
api_updateAPIPanel API updated — only on a real package change since 5.4.0
api_upgradeAPIPanel API upgraded
api_sslAPIPanel API SSL installed
git_configureGitGit provider token configured
sync_exportSyncApps exported
sync_importSyncApps imported
sync_pushSyncApps pushed to remote
service_restartServicesService restarted
service_startServicesService started
service_stopServicesService stopped
search_installSearchMeilisearch installed (5.2.2)
search_enableSearchSearch enabled for an app (5.2.2)
search_disableSearchSearch disabled for an app (5.2.2)
search_key_rotateSearchSearch API key rotated (5.2.2)
search_upgradeSearchMeilisearch upgraded (5.2.2)
search_removeSearchMeilisearch removed (5.2.2)
package_installPackagesHost package installed (5.2.2)
package_removePackagesHost package removed (5.2.2)

Automatic alerts

Once configured, Cipi sends email notifications on:

  • Backup errors (S3 upload failures, dump errors, a corrupt archive) and, since v5.1.0, backups that are simply overdue (backup_stale)
  • Deploy results — since v5.1.0 both success and failure, from the CLI and the Git webhook alike, with the branch, release number, commit, author, duration and the post-deploy healthcheck verdict in the body
  • A release that failed its post-deploy healthcheck (deploy_health_fail), including what an automatic rollback did about it
  • System cron job failures (via the cipi-cron-notify wrapper)
  • App lifecycle events — notifies when an app is created, edited, or deleted, including server hostname, app name, domain, and PHP version
  • Sudo and su elevation — notifies when any user successfully elevates via sudo or su, including who ran it, target user (for su), SSH key, client IP, and TTY
  • Privileged SSH login — notifies when root or any sudoer logs in via SSH, including source IP, SSH key fingerprint, and key comment
  • SSH key changes — notifies when an SSH key is added to, removed from, or renamed on the cipi user, including hostname, IP, fingerprint, key comment, timestamp, and remaining key count. Rename alerts also include the old and new key name.

Every email notification includes a footer with the client IP (SSH_CLIENT) and the SSH key name used to authenticate, when applicable. The key name is resolved via SSH_USER_AUTH with an auth.log fallback when needed.

Security auth notifications

Cipi integrates PAM-based authentication notifications via pam_exec.so with ExposeAuthInfo enabled. When SMTP is configured, the system automatically sends email alerts on these security-relevant events:

  • Sudo and su elevation — triggered when any user successfully runs sudo or su. The notification includes the username, target user (for su), the TTY, SSH key, client IP, and timestamp.
  • Privileged SSH login — triggered when root or any user in the sudo group logs in via SSH. The notification includes the username, source IP address, SSH key fingerprint, and key comment (resolved from /var/log/auth.log fingerprint matching against authorized_keys).
  • SSH key changes — triggered when an SSH key is added to, removed from, or renamed on the cipi user via cipi ssh add, cipi ssh remove, or cipi ssh rename. The notification includes the hostname, server IP, key fingerprint, key comment, timestamp, and remaining key count. Rename alerts also include the old and new key name.
  • App lifecycle events — triggered when an app is created, edited, or deleted. The notification includes the server hostname, app name, domain, and PHP version.

Notifications run asynchronously in the background so they never delay login or command execution. If SMTP is not configured, the hooks fail silently with no impact on the system.

Security event log

Regardless of SMTP configuration, all notification events (SSH key changes, app lifecycle, password resets, sudo/su/SSH login, cron failures) are always logged to /var/log/cipi/events.log in a compact one-line format. The log is rotated daily with 1-year retention via logrotate.

Cron wrapper

The cipi-cron-notify utility wraps system cron jobs and sends a notification if the job exits with a non-zero code. This is useful for monitoring critical scheduled tasks.

Chat alert channels

Since v5.3.0, email is no longer the only way out of Cipi's notifications. Every trigger — deploys, backups, scans, SSH logins, healthchecks and monitor alerts — also fans out to Slack, Discord, Telegram, ntfy or a custom webhook. Channels do not need SMTP. And nothing changes until you add one: with no channel configured, delivery is email-only, exactly as before.

bash
$ cipi notifications channel add slack ops --url=https://hooks.slack.com/services/…
$ cipi notifications channel add discord team --url=https://discord.com/api/webhooks/…
$ cipi notifications channel add telegram oncall --token=<bot-token> --chat-id=<id>
$ cipi notifications channel add ntfy phone --url=https://ntfy.sh/my-topic --priority=high
$ cipi notifications channel add webhook pager --url=https://example.com/hooks/cipi

$ cipi notifications channel list [--json]   # secrets masked
$ cipi notifications channel test ops        # or: test all
$ cipi notifications channel disable ops     # keep it, stop sending
$ cipi notifications channel enable ops
$ cipi notifications channel remove ops

Channel ids are lowercase letters, digits and dashes, and unique on the server.

Type Options What arrives
slack --url= — an Incoming Webhook A text message: the subject in bold, then the body.
discord --url= — a channel webhook The same, truncated at Discord's 2,000-character cap.
telegram --token= — the bot token from @BotFather; --chat-id= — send /start to the bot, then read getUpdates (negative for groups) Plain text through the Bot API — no Markdown, so an underscore in a hostname cannot break it — truncated at Telegram's 4,096-character cap.
ntfy --url= — the topic URL; optional --priority=min|low|default|high|urgent Title, Priority and Tags headers. Urgent triggers — security events, failures, most monitor alerts — raise a default-priority channel to high.
webhook --url= Structured JSON, for anything else.
json
// POST body sent to a webhook channel
{
  "server": "web-01",
  "trigger": "deploy_fail",
  "subject": "…",
  "body": "…",
  "ts": 1789123456
}

Best-effort, never blocking

Each channel gets a 5-second timeout, and a failed delivery is only logged to /var/log/cipi/events.log: a slow or dead webhook can never stall a deploy or a cron run. Channels are stored encrypted in /etc/cipi/alerts.json; channel list masks the secret part of webhook URLs and shows only the start of a bot token.

Channels follow the same triggers as email: cipi notifications disable <trigger> mutes that event everywhere, and there is no per-channel filter. A chat message carries the same body as the email, footer included.

Migration 5.3.0 writes an empty alerts.json and configures no channel and no trigger. To see what an alert looks like everywhere at once, run cipi monitor test.

Log Retention (GDPR)

Cipi enforces automatic log rotation policies designed to meet GDPR and general data protection requirements. Logs are rotated and deleted automatically — no manual cleanup needed.

Category Logs Retention
Application Laravel, PHP-FPM, workers, deploy, system 12 months
Security Fail2ban, UFW firewall, authentication, Cipi events (events.log) 12 months
HTTP / Navigation Nginx access and error logs 90 days
HTTP/navigation logs (nginx access logs) contain IP addresses, which are personal data under GDPR. The 90-day retention ensures compliance with the data minimization principle while preserving enough history for debugging and security analysis. Application and security logs are retained for 12 months to support audit trails and incident investigation.

Valkey

Valkey is the in-memory data store that Cipi installs as part of the default stack. Since v4.5.6 Cipi provisions Valkey instead of redis-server. It excels at caching, session storage, message queues, real-time broadcasting, and rate limiting.

Why Valkey instead of Redis

Valkey is the truly open-source, BSD-licensed fork of Redis, stewarded by the Linux Foundation. It was created in 2024 after Redis Inc. relicensed Redis away from the permissive BSD license to the source-available SSPL / RSALv2 — a change that no longer met the open-source definition. Backed by AWS, Google Cloud, Oracle, and a large community, Valkey continues the same battle-tested codebase under a license that stays free forever. That makes it a perfect fit for Cipi's MIT, no-vendor-lock-in philosophy, and it ships natively in Ubuntu 24.04's Universe repository (packages valkey-server + valkey-tools) — no third-party PPA to trust.

Just as importantly, Valkey is a drop-in replacement: it speaks the exact same RESP protocol on the same port (127.0.0.1:6379), honours the same requirepass / bind directives, and reads the same RDB/AOF data format. Your apps need zero changes — the phpredis extension and your existing REDIS_* .env values keep working exactly as before.

How Cipi implements it

  • Installsetup.sh installs and configures Valkey (/etc/valkey/valkey.conf, service valkey-server), bound to localhost only and protected with a password.
  • Service managementcipi service … manages valkey-server (the names redis-server, redis, and valkey are still accepted as aliases). It is added to the unattended-upgrades blacklist, so Cipi manages it instead of an automatic upgrade.
  • Credentials — stored under valkey_user / valkey_password in /etc/cipi/server.json (the legacy redis_* keys are still read as a fallback). Host: 127.0.0.1, Port: 6379.
  • Password resetcipi reset valkey-password regenerates the password and restarts the service (cipi reset redis-password stays as an alias).

Migrating from Redis (4.5.6 / 4.5.7)

Existing servers are switched to Valkey automatically on cipi self-update — no app .env editing required. The migration reuses the current Redis password (recovered from server.json or /etc/redis/redis.conf), forces an RDB SAVE and snapshots dump.rdb/AOF, purges redis-server, installs valkey-server + valkey-tools on the same port with the same requirepass / bind, restores the dataset, and rewrites server.json (redis_*valkey_*) and the unattended-upgrades blacklist — so cache, sessions, and queued jobs survive the switch.

v4.5.7 corrects the package name to valkey-server (the Ubuntu 24.04 daemon package; 4.5.6 initially used valkey) and makes the migration fully self-contained and safe. It auto-enables the universe APT component when the package isn't found, runs a post-start health check (PINGPONG with the password), and rolls back to redis-server — restoring both the saved password and the dataset — if Valkey can't be installed or doesn't come up healthy. The dataset snapshot is kept until the switch is verified, then cleaned up. The migration is idempotent: servers already on Valkey skip it.

Laravel integration

Add these variables to your .env via cipi app env myapp. The variable names stay REDIS_* — that is what phpredis and Laravel's redis driver expect, and Valkey answers on the very same socket:

env
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=your-password-from-server-json
REDIS_PORT=6379

Then set the drivers for each use case:

  • CacheCACHE_STORE=redis
  • SessionSESSION_DRIVER=redis
  • QueueQUEUE_CONNECTION=redis (then cipi worker restart myapp)
  • BroadcastingBROADCAST_CONNECTION=redis

Install the phpredis PHP extension for best performance, or use predis/predis as a pure-PHP fallback. Both talk to Valkey transparently.

Self-Update

Cipi can update itself from GitHub without affecting any app, database, or configuration.

bash
$ cipi self-update --check   # check for a new version
$ cipi self-update           # update to latest

Update process

  1. Downloads the latest version from GitHub
  2. Backs up the current installation to /opt/cipi.bak.YYYYMMDDHHMMSS/
  3. Replaces CLI and lib scripts
  4. Runs any pending migration scripts in order (e.g. new Nginx directives, new packages)
  5. Updates the version file

Migration scripts live in lib/migrations/ and are named by version (e.g. 4.1.0.sh, 5.0.18.sh). When updating from v4.0.0 to v4.2.0, Cipi automatically runs 4.1.0.sh and 4.2.0.sh in order. Your apps, databases, and configurations are never touched.

Recent migrations improve reliability without changing app data: 5.0.6 — default IP whitelist file and regenerated API sudoers; 5.0.9cipi php switch in sudoers for PUT /api/php/default; 5.0.13 — reclaim API/GUI ownership after update; 5.0.14–5.0.17 — timed GitHub/Packagist panel package updates (no more hung cipi self-update on API/GUI Composer VCS clones); 5.0.18 — repair Panel GUI after symlink/open_basedir HTTP 500 (cipi gui fix-permissions). Run cipi self-update to reach 5.1.0.

Migration 5.1.0

The 5.1.0 migration is the largest of the 5.x line. On an existing server it:

  • installs the automatic-deploy wrapper and repoints each app's webhook cron at it;
  • backfills the CLI 99-cipi.ini and rewrites FPM pools so they inherit the server-wide file — see cipi ini;
  • converts the hardcoded nightly backup job into a default backup profile, carrying over its existing --weeks retention;
  • takes over the backup schedule with the managed crontab block;
  • claims the :443 default server when nothing else does.

Every step degrades to a warning and carries on. That matters: on an early 5.1.0 build a grep that matched nothing could fail the migration under set -o pipefail, and a failed migration makes cipi self-update refuse the release and retry it, failing again, every night.

Migrations 5.4.0 and 5.4.1

5.4.0 turns on the deploy audit ledger for existing apps: it installs cipi-deploy-audit, adds one sudoers rule per app (validated with visudo and restored if rejected), appends the audit hooks to each deploy.php without touching the rest of the file, switches the deploy-trigger cron from rm to mv, and writes the audit start marker.

It also moves servers off Node 20, which reached end of life in April 2026:

  • Fresh installs: setup.sh installs Node 22 from NodeSource.
  • Servers still on Node 20 (the NodeSource package, or cipi node default 20) are switched with cipi node default 22, which downloads and verifies the official build and links it into /usr/local/bin ahead of /usr/bin. The NodeSource package is not touched, so cipi node default system restores 20. Laravel asset builds, cipi app run npm and deploy.post use 22 from their next run.
  • Servers on another major, or with no Node at all, are left as they are.
  • A failed download leaves the server on 20, prints how to retry and lets the update carry on. A node_default notification is sent.

5.4.1 regenerates /etc/sudoers.d/cipi-api so the panel API can drive redirects, proxies and Node apps. Neither migration deploys, reloads or restarts anything.

When the update itself fails

Up to 5.0.x, cipi self-update reported only “Download failed” — git's stderr was discarded, so the one message that explains what happened (no DNS, no outbound HTTPS, a missing branch, a full disk, an expired CA bundle) never reached anyone. Since v5.1.0 the real error is printed, the likely cause is named, and the command to reproduce it by hand is shown. The clone also runs under a 180-second timeout instead of being able to hang indefinitely, and a missing git is reported as such. An unattended nightly update now also sends the self_update notification, so it no longer lands without a word. Since v5.2.1 that mail is decided from a snapshot taken before /etc/cipi/version is rewritten, and is sent only when that snapshot differs from the downloaded version. A nightly refresh that stays on the same release is silent. 5.2.1 itself has no migration: self-update copies the libs and refreshes shell completion.

For example, the 4.5.5 migration retro-fits existing apps with the new ll='ls -al' shell alias: it appends the alias to each app's ~/.bashrc once (only when missing, preserving ownership), so apps created before 4.5.5 get it on the next cipi self-update.

Automatic maintenance crons

Cipi schedules several root-level jobs during installation. App-level crontabs (scheduler, deploy trigger) are separate — see User crontab.

Schedule Job
Daily 02:00 cipi backup run — S3 backups for all apps
Daily 03:00 cipi backup prune --weeks=4
Sun 03:30 cipi php upgrade — security patches for all installed PHP versions (wrapped by cipi-cron-notify)
Daily 03:50 cipi self-update (wrapped by cipi-cron-notify)
Sun 04:10 cipi ssl renew
Daily 04:15 Panel API maintenance (cipi-api-maintain — prune jobs/metrics)
Daily 04:30 cipi api update — soft-update panel Laravel + cipi/api

Wildcard domains

Cipi does not support wildcard domains (*.myapp.com) natively. The block is twofold and architectural — not a configuration detail.

Why wildcards are not supported

1 — Domain validation rejects *
Every domain passed to cipi alias add (and cipi app create) is validated against a strict regex that requires the string to start with [a-zA-Z0-9]. The asterisk fails immediately, before nginx or certbot are ever touched.

2 — Certbot uses HTTP-01 challenge, which cannot issue wildcard certs
cipi ssl install calls certbot --nginx, which relies on the HTTP-01 (or TLS-ALPN-01) challenge — placing a verification file on disk and serving it over port 80. Let's Encrypt only issues wildcard certificates via the DNS-01 challenge, which requires programmatic access to your DNS provider's API. Cipi does not integrate with any DNS provider, so even if the validation were bypassed, certbot would refuse to issue the wildcard cert.

Recommended alternative — Multi-SAN certificate

If your subdomains are fixed and enumerable (e.g. api, admin, www, staging), the correct approach is to add each one as an explicit alias and let Cipi issue a single SAN certificate covering all of them:

bash
$ cipi alias add myapp api.myapp.com
$ cipi alias add myapp admin.myapp.com
$ cipi alias add myapp www.myapp.com
$ cipi ssl install myapp   # single cert, SAN covers all domains

Certbot's --expand flag (used internally by Cipi) adds the new SANs to the existing certificate without issuing a new one. The SAN list has no meaningful limit for typical use.

Manual wildcard certificate (outside Cipi)

If you need dynamic subdomains (e.g. <tenant>.saas.com), you can obtain a wildcard certificate manually using a DNS plugin for certbot and place it on the server. Cipi will not manage, renew, or track it — you own the lifecycle entirely.

bash
# example with the Cloudflare DNS plugin
$ pip install certbot-dns-cloudflare
$ certbot certonly --dns-cloudflare \
    --dns-cloudflare-credentials /root/.cloudflare.ini \
    -d "*.myapp.com" -d "myapp.com"

After obtaining the certificate, edit the nginx vhost for the app directly (/etc/nginx/sites-available/myapp) to reference the wildcard cert paths and add server_name *.myapp.com myapp.com;. Then reload nginx:

bash
$ nginx -t && systemctl reload nginx
Running cipi ssl install myapp after manual wildcard setup will overwrite your custom nginx SSL directives with a Let's Encrypt HTTP-01 certificate. If you manage a wildcard cert manually, avoid running cipi ssl install on that app.

Edit Nginx config

To customise the Nginx vhost for an app, edit the site config directly. After changes, test and reload Nginx.

bash
$ sudo nano /etc/nginx/sites-available/<app>
$ sudo nginx -t && sudo systemctl reload nginx

Uninstall Cipi

Cipi does not provide a built-in uninstall command. If you need to completely remove Cipi from a server, follow the steps below in order. This procedure removes every component that Cipi installs — users, services, packages, configs, and data.

This is a destructive and irreversible operation. All apps, databases, SSL certificates, and server configurations managed by Cipi will be permanently deleted. Back up everything you need before proceeding. After uninstalling, the recommended approach is to reprovision the server from scratch.

1 — Stop and remove all apps

For each app managed by Cipi, remove its system user, home directory, database, nginx vhost, PHP-FPM pool, and supervisor config.

bash
# List all app users (members of cipi-apps group)
$ grep cipi-apps /etc/group

# For EACH app user, remove everything
$ supervisorctl stop <app_user>:*
$ rm -f /etc/supervisor/conf.d/<app_user>.conf
$ rm -f /etc/nginx/sites-enabled/<app_user>
$ rm -f /etc/nginx/sites-available/<app_user>
$ rm -f /etc/php/*/fpm/pool.d/<app_user>.conf
$ rm -f /etc/sudoers.d/cipi-<app_user>
$ mysql -e "DROP DATABASE IF EXISTS <app_user>; DROP USER IF EXISTS '<app_user>'@'localhost'; DROP USER IF EXISTS '<app_user>'@'127.0.0.1';"
$ userdel -r <app_user>

2 — Remove the Cipi user and groups

bash
$ userdel -r cipi
$ groupdel cipi-ssh 2>/dev/null
$ groupdel cipi-apps 2>/dev/null

3 — Remove Cipi binaries, libraries, and data

bash
$ rm -f /usr/local/bin/cipi
$ rm -f /usr/local/bin/cipi-worker
$ rm -f /usr/local/bin/cipi-cron-notify
$ rm -f /usr/local/bin/cipi-auth-notify
$ rm -rf /opt/cipi
$ rm -rf /etc/cipi
$ rm -rf /var/log/cipi

4 — Remove Cipi API (if installed)

bash
$ systemctl stop cipi-queue 2>/dev/null
$ systemctl disable cipi-queue 2>/dev/null
$ rm -f /etc/systemd/system/cipi-queue.service
$ systemctl daemon-reload

5 — Remove Cipi cron jobs

bash
# Edit root crontab and remove all Cipi entries
$ crontab -e
# Remove lines referencing: cipi self-update, certbot renewal, cache cleanup, RAM drop

6 — Remove Cipi configuration files

bash
# Sudoers
$ rm -f /etc/sudoers.d/cipi-sudo
$ rm -f /etc/sudoers.d/cipi-api

# Logrotate
$ rm -f /etc/logrotate.d/cipi-app-logs
$ rm -f /etc/logrotate.d/cipi-http-logs
$ rm -f /etc/logrotate.d/cipi-security-logs

# Unattended upgrades
$ rm -f /etc/apt/apt.conf.d/50cipi-unattended-upgrades
$ rm -f /etc/apt/apt.conf.d/20cipi-auto-upgrades

# System profile and MOTD
$ rm -f /etc/profile.d/cipi-env.sh
$ echo "" > /etc/motd

# MariaDB custom config
$ rm -f /etc/mysql/mariadb.conf.d/99-cipi.cnf

# PHP custom config (all versions)
$ rm -f /etc/php/*/fpm/conf.d/99-cipi.ini

# Nginx default page
$ rm -f /etc/nginx/sites-available/default
$ rm -f /etc/nginx/sites-enabled/default

7 — Purge installed packages

Remove all packages that Cipi installed. Skip any package you want to keep for other purposes.

bash
$ systemctl stop nginx mariadb valkey-server fail2ban supervisor
$ systemctl stop php*-fpm

$ apt purge -y nginx* mariadb-server mariadb-client valkey-server \
    fail2ban supervisor certbot python3-certbot-nginx \
    php8.4* php8.5* nodejs

$ apt autoremove -y
$ apt autoclean

8 — Remove APT repositories

bash
$ add-apt-repository --remove ppa:ondrej/php -y
$ rm -f /etc/apt/sources.list.d/mariadb.list
$ rm -f /etc/apt/sources.list.d/nodesource.list
$ rm -f /etc/apt/keyrings/mariadb-keyring.pgp
$ apt update

9 — Remove Composer and Deployer

bash
$ rm -f /usr/local/bin/composer
$ rm -f /usr/local/bin/dep

10 — Remove swap file

bash
$ swapoff /var/swap.1
$ rm -f /var/swap.1
# Remove the swap entry from /etc/fstab
$ sed -i '/swap\.1/d' /etc/fstab

11 — Restore SSH and PAM defaults

Cipi hardens SSH (disables root login and password auth) and adds PAM hooks. If you need to restore defaults:

bash
# Restore sshd_config to allow password auth (if needed)
$ sed -i 's/^PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config
$ sed -i 's/^PermitRootLogin no/PermitRootLogin yes/' /etc/ssh/sshd_config

# Remove Cipi PAM hooks
$ sed -i '/cipi-auth-notify/d' /etc/pam.d/sshd
$ sed -i '/cipi-auth-notify/d' /etc/pam.d/sudo

# Restore sysctl
$ sed -i '/vm.swappiness/d' /etc/sysctl.conf
$ sysctl -p

$ systemctl restart sshd

12 — Reset firewall

bash
$ ufw disable
$ ufw reset
After a full uninstall, the server will be stripped of its web stack and security hardening. The recommended approach is to reprovision the server from a clean OS image rather than trying to reconfigure the same machine. Use this guide primarily to clean up before a fresh start, or to selectively remove Cipi components while keeping packages you still need.