Advanced
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, whitelistedapp 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:
$ 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
$ 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:
$ 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 appsapps-create— create appsapps-edit— edit apps (PHP, repository, branch, primary domain since API 1.9.0+ / Cipi 4.6.2+)apps-suspend— suspend and unsuspend appsapps-basicauth— enable, disable, and inspect HTTP Basic Auth on apps (API 1.10.0+)apps-env— list/merge app.envkeys (API 1.14.0+ / Cipi 5.0.3+)apps-auth— manage shared Composerauth.json(API 1.14.0+; distinct fromapps-basicauth)apps-artisan— run Artisan as an async job (API 1.14.0+)apps-run— whitelisted non-interactiveapp 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 thecipiuser (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 appsdeploy-manage— deploy, rollback, unlock, and read the deploy audit ledger (API 1.31.0+ / Cipi 5.4.0+)ssl-manage— install and manage SSL certificatesaliases-view— read aliasesaliases-create— add aliasesaliases-delete— remove aliaseswww-manage— www/apex counterpart and redirects (API 1.12.0+ / Cipi 4.8+)dbs-view— list databasesdbs-create— create databasesdbs-manage— backup, restore, regenerate password, install an enginestatus-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 engines — POST /api/dbs/engines/install (ability
dbs-manage; API 1.15.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 keys — GET|POST /api/ssh/keys,
DELETE /api/ssh/keys/{n} (abilities ssh-view / ssh-manage;
API 1.15.0+).
Services — GET /api/services,
POST /api/services/{name}/restart (abilities services-view /
services-manage; API 1.15.0+).
SMTP — GET|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=…).
Healthchecks — GET /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):
export CIPI_API_URL="https://api.myserver.com" export CIPI_API_TOKEN="your-sanctum-token"
List apps (sync, 200):
curl -sS "${CIPI_API_URL}/api/apps" \ -H "Authorization: Bearer ${CIPI_API_TOKEN}" \ -H "Accept: application/json"
Server status (sync, requires status-view):
curl -sS "${CIPI_API_URL}/api/status" \ -H "Authorization: Bearer ${CIPI_API_TOKEN}"
App logs (sync, requires apps-view, API 1.11.9+):
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):
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):
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):
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):
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):
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):
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):
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):
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— runssudo cipi db list(sync). Requires Cipi 4.4.17+ (migration addscipi 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/ MCPServerStatus— prefersudo cipi status(API 1.11.8+); host-read fallback when sudo fails (includespostgresqlsince API 1.12.1+).- MCP
ServiceList—sudo cipi service list - MCP
AppArtisan—sudo 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-apioncipi 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-apioncipi self-update). - Since Cipi 5.2.2+:
search status|list|enable|disableandpackage listin the sudoers whitelist.search install|upgrade|key rotate|removeandpackage install|removestay off it — they change or destroy server state and stay with the operator on the CLI. - Since Cipi 5.2.3+: the manual
stack upgrades —
nginx upgrade,db upgradeandservice upgrade— are not in the sudoers whitelist either, for the same reason aspackage install: restarting a database engine is an operator decision, taken at the CLI while you are watching. - Since Cipi 5.3.0+:
zt statusandmonitor listin the sudoers whitelist.zt enable,lockand the rest, andmonitor enable|disable|set, stay with the operator on the CLI — the same rule aspackage install. - Since Cipi 5.4.1+ (API 1.31.0+):
redirect set|enable|disable|unset|add|remove|list,proxy add|remove|listandnode list|status|restart— each rule the usual exact command line.deploy <app> --auditreads the root-only ledger. Migration 5.4.1 regenerates/etc/sudoers.d/cipi-apiand checks it withvisudo -cf. What deliberately stays CLI-root: Node runtime management (node install|default|upgrade|remove),search install|upgrade|key-rotate,package install|removeand everyztmutation — 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=).
$ 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(optionalengine,octane),AppEdit,AppSuspend,AppUnsuspend,AppDelete,AppDeploy,AppDeployRollback,AppDeployUnlock,AppArtisan(Laravel apps only; rejects custom apps andtinker),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(optionalengineon list/mutations; API 1.12.0+;DbDeleteremoved in 1.19.0) - SSL:
SslInstall,SslForce(API 1.12.0+) - Jobs & logs:
JobShow(poll async job status, parsedresult, and CLI output),AppLogs(recent app logs by type:all,nginx,php,worker,deploy,laravel— same ascipi app logs; REST equivalent:GET /api/apps/{name}/logssince API 1.11.9+),ApiLogShow(recent Laravel logs for the panel API host) - Server monitoring:
ServerStatus(structured JSON matchingGET /api/status/cipi status),ServiceList(system service status viacipi service list)
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:
- Configure the API with
cipi api <domain>andcipi api ssl - Create a token with
cipi api token createand select at leastmcp-access - Add the MCP server to your client config (see below)
Cursor
Add to ~/.cursor/mcp.json (or Cursor → Settings → MCP):
{
"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:
{
"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:
$ 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:
{
"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>andcipi 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
- Copy
modules/servers/cipi/into your WHMCS root:your-whmcs/ └── modules/ └── servers/ └── cipi/ ├── cipi.php └── lib/ └── CipiApiClient.php - 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)
- 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
// 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.
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
$ 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.
# 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"
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.
# 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.
# 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:
- Linux user — Creates a new user with a random password
- Directories — Creates
/home/<app>/shared/,logs/,.ssh/,.deployer/ - SSH deploy key — Restores from archive (same key works with GitHub/GitLab without reconfiguration)
- MariaDB database — Creates database + user with a new random password
- Database data — Imports the dump if
--with-dbwas used during export .env— Copies from archive, then overwritesDB_PASSWORD,DB_USERNAME,DB_DATABASE,DB_HOSTwith the new server's values. Everything else (APP_KEY,MAIL_*,REDIS_*, custom vars) stays as-is- PHP-FPM pool, Nginx vhost, Supervisor workers, Crontab, Deployer — Fully configured from archive data
Safety checks before import
The import runs pre-flight checks before touching anything:
- App already exists — blocked unless
--updateis 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.
$ cipi sync import /tmp/archive.tar.gz.enc --update --passphrase="MyStr0ngP@ss"
What update does for an existing app
.envsync — The archive.envreplaces the local one, butDB_PASSWORD,DB_USERNAME,DB_DATABASE, andDB_HOSTare 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
.envautomatically. - Nginx vhost, Supervisor workers, Deployer config — Regenerated from archive data.
- Deploy — If
--deployis passed, runsdep deployto 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 installseparately)
List (inspect archive)
View what is inside an archive without importing anything.
$ 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.
# 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
- Step 1: Runs
cipi sync exportlocally (encrypts with passphrase) - Step 2: Transfers the encrypted archive to the target via rsync
- Step 3: If
--importis passed, runscipi sync import --update --yeson 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:
# 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.
# 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:
# 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.
# 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
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
# 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)
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 installafter 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
# 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.
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
$ 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.
$ 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.
// 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_create | Apps | App created |
app_edit | Apps | App modified |
app_delete | Apps | App deleted |
app_suspend | Apps | App suspended |
app_unsuspend | Apps | App unsuspended |
app_ssh_password_reset | Apps | App SSH password reset |
app_db_password_reset | Apps | App DB password reset |
alias_add | Domains | Alias added |
alias_remove | Domains | Alias removed |
www_add | Domains | WWW alias added |
www_force_to_root | Domains | WWW force to-root |
www_force_from_root | Domains | WWW force from-root |
www_clear | Domains | WWW redirect cleared |
redirect_change | Domains | App or path redirect changed (5.3.1) |
proxy_change | Domains | Proxy prefix added or removed (5.3.1) |
node_default | Node | Server-wide Node version changed (5.4.0) |
auth_create | Auth | Composer auth.json created |
auth_edit | Auth | Composer auth.json edited |
auth_delete | Auth | Composer auth.json deleted |
basicauth_enable | Basic auth | HTTP basic auth enabled |
basicauth_disable | Basic auth | HTTP basic auth disabled |
deploy_success | Deploy | Deploy succeeded |
deploy_fail | Deploy | Deploy failed |
deploy_rollback | Deploy | Deploy rollback |
deploy_snapshot_fail | Deploy | Pre-deploy DB snapshot failed |
health_fail | Health | HTTP healthcheck failed (periodic, after 3 failures) |
deploy_health_fail | Health | Post-deploy healthcheck failed |
monitor_disk | Monitor | Disk usage over threshold (5.3.0) |
monitor_ssl | Monitor | SSL certificate expiring (5.3.0) |
monitor_services | Monitor | System service not running (5.3.0) |
monitor_workers | Monitor | Queue worker / Horizon not running (5.3.0) |
monitor_http_5xx | Monitor | HTTP 5xx spike in access logs (5.3.0) |
monitor_fs | Monitor | Filesystem read-only / config not writable (5.3.0) |
monitor_load | Monitor | Load average over threshold (5.3.0) |
monitor_ok | Monitor | Monitor check recovered (5.3.0) |
ssl_install | SSL | SSL certificate installed |
ssl_force | SSL | HTTP → HTTPS redirect forced |
ssl_renew | SSL | SSL certificates renewed |
php_install | PHP | PHP version installed |
php_switch | PHP | System PHP switched |
php_remove | PHP | PHP version removed |
php_upgrade | PHP | PHP packages upgraded |
nginx_upgrade | Stack | nginx packages upgraded (5.2.3) |
mariadb_upgrade | Stack | MariaDB packages upgraded (5.2.3) |
pgsql_upgrade | Stack | PostgreSQL packages upgraded (5.2.3) |
valkey_upgrade | Stack | Valkey packages upgraded (5.2.3) |
db_create | Database | Database created |
db_delete | Database | Database deleted |
worker_add | Workers | Worker added |
worker_remove | Workers | Worker removed |
ssh_key_add | SSH keys | SSH key added |
ssh_key_rename | SSH keys | SSH key renamed |
ssh_key_remove | SSH keys | SSH key removed |
ssh_login | Security | SSH login (cipi/root/sudo users) |
sudo | Security | Sudo elevation |
su | Security | su to root by cipi |
zt_enable | Security | Cloudflare Zero Trust enabled (5.3.0) |
zt_disable | Security | Cloudflare Zero Trust disabled (5.3.0) |
zt_lock_http | Security | HTTP ports locked (Cloudflare) (5.3.0) |
zt_lock_ssh | Security | SSH port closed (Cloudflare Tunnel) (5.3.0) |
backup_fail | Backup | Backup failed |
backup_stale | Backup | Backup overdue (no successful run in its window) (5.1.0) |
cron_fail | Cron | Cron job failed |
ini_set | PHP | PHP setting changed (5.1.0) |
yml_apply | cipi.yml | cipi.yml applied (5.1.0) |
yml_fail | cipi.yml | cipi.yml invalid or could not be applied (5.1.0) |
yml_post_fail | cipi.yml | A deploy.post step failed (5.2.3) |
self_update | Updates | Cipi updated itself (5.1.0) |
reset_root_password | Reset | Root SSH password reset |
reset_db_password | Reset | MariaDB root password reset |
reset_valkey_password | Reset | Valkey password reset |
api_configure | API | Panel API configured |
api_update | API | Panel API updated — only on a real package change since 5.4.0 |
api_upgrade | API | Panel API upgraded |
api_ssl | API | Panel API SSL installed |
git_configure | Git | Git provider token configured |
sync_export | Sync | Apps exported |
sync_import | Sync | Apps imported |
sync_push | Sync | Apps pushed to remote |
service_restart | Services | Service restarted |
service_start | Services | Service started |
service_stop | Services | Service stopped |
search_install | Search | Meilisearch installed (5.2.2) |
search_enable | Search | Search enabled for an app (5.2.2) |
search_disable | Search | Search disabled for an app (5.2.2) |
search_key_rotate | Search | Search API key rotated (5.2.2) |
search_upgrade | Search | Meilisearch upgraded (5.2.2) |
search_remove | Search | Meilisearch removed (5.2.2) |
package_install | Packages | Host package installed (5.2.2) |
package_remove | Packages | Host 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-notifywrapper) - 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
sudoorsu, including who ran it, target user (forsu), SSH key, client IP, and TTY - Privileged SSH login — notifies when
rootor 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
cipiuser, 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
sudoorsu. The notification includes the username, target user (forsu), the TTY, SSH key, client IP, and timestamp. - Privileged SSH login — triggered when
rootor any user in thesudogroup logs in via SSH. The notification includes the username, source IP address, SSH key fingerprint, and key comment (resolved from/var/log/auth.logfingerprint matching againstauthorized_keys). - SSH key changes — triggered when an SSH key is added to, removed from, or
renamed on the
cipiuser viacipi ssh add,cipi ssh remove, orcipi 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.
$ 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. |
// 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.
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 |
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
- Install —
setup.shinstalls and configures Valkey (/etc/valkey/valkey.conf, servicevalkey-server), bound tolocalhostonly and protected with a password. - Service management —
cipi service …managesvalkey-server(the namesredis-server,redis, andvalkeyare 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_passwordin/etc/cipi/server.json(the legacyredis_*keys are still read as a fallback). Host: 127.0.0.1, Port: 6379. - Password reset —
cipi reset valkey-passwordregenerates the password and restarts the service (cipi reset redis-passwordstays 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 (PING → PONG 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:
REDIS_HOST=127.0.0.1 REDIS_PASSWORD=your-password-from-server-json REDIS_PORT=6379
Then set the drivers for each use case:
- Cache —
CACHE_STORE=redis - Session —
SESSION_DRIVER=redis - Queue —
QUEUE_CONNECTION=redis(thencipi worker restart myapp) - Broadcasting —
BROADCAST_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.
$ cipi self-update --check # check for a new version $ cipi self-update # update to latest
Update process
- Downloads the latest version from GitHub
- Backs up the current installation to
/opt/cipi.bak.YYYYMMDDHHMMSS/ - Replaces CLI and lib scripts
- Runs any pending migration scripts in order (e.g. new Nginx directives, new packages)
- 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.9 — cipi 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.iniand rewrites FPM pools so they inherit the server-wide file — seecipi ini; - converts the hardcoded nightly backup job into a
defaultbackup profile, carrying over its existing--weeksretention; - takes over the backup schedule with the managed crontab block;
- claims the
:443default 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.shinstalls Node 22 from NodeSource. - Servers still on Node 20 (the NodeSource package, or
cipi node default 20) are switched withcipi node default 22, which downloads and verifies the official build and links it into/usr/local/binahead of/usr/bin. The NodeSource package is not touched, socipi node default systemrestores 20. Laravel asset builds,cipi app run npmanddeploy.postuse 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_defaultnotification 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:
$ 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.
# 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:
$ nginx -t && systemctl reload nginx
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.
$ 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.
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.
# 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
$ userdel -r cipi $ groupdel cipi-ssh 2>/dev/null $ groupdel cipi-apps 2>/dev/null
3 — Remove Cipi binaries, libraries, and data
$ 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)
$ 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
# 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
# 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.
$ 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
$ 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
$ rm -f /usr/local/bin/composer $ rm -f /usr/local/bin/dep
10 — Remove swap file
$ 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:
# 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
$ ufw disable $ ufw reset