cipi php

PHP 8.5 is pre-installed during setup. Additional versions can be added at any time; since v4.5.12 Cipi probes APT sources per Ubuntu codename — typically the ondrej/php PPA on 24.04, packages.sury.org on 26.04 when Launchpad has no suite, or Ubuntu main as a last resort (co-installable 8.3–8.5 where the chosen repo supports it).

Since v4.5.4 Cipi bundles Deployer 8, which requires PHP ≥ 8.3. cipi php install, cipi php switch, cipi app create and cipi app edit therefore accept only 8.3, 8.4 and 8.5. Legacy versions (7.4–8.2) remain detectable and removable, so you can still clean up a pre-4.5.4 server with cipi php remove <old-version>.
bash
$ cipi php list              # list installed versions, status, and system default
$ cipi php install 8.4       # install an additional PHP version (8.3–8.5)
$ cipi php switch 8.4        # set system default (root/cipi, API pool)
$ cipi php remove 8.1        # remove a version, incl. legacy (blocks if default or apps use it)
$ cipi php upgrade           # apply security patches to all installed PHP packages

Security patch upgrades

Since v4.7.13, PHP packages are excluded from unattended-upgrades and managed by Cipi instead. cipi php upgrade runs apt-get update and --only-upgrade on every installed php* and libphp* package, then restarts the affected PHP-FPM pools. When SMTP is configured and packages were upgraded, Cipi sends an email (php_upgrade trigger; disable with cipi notifications disable php_upgrade).

A weekly check runs automatically on Sunday at 03:30 via root crontab (wrapped by cipi-cron-notify). Log: /var/log/cipi/php-upgrade.log. Existing servers receive the cron on cipi self-update (migration 4.7.13).

System default vs app-specific PHP

The system default is the PHP version used by the root and cipi users, the Cipi API FPM pool, and the API queue worker. Use cipi php switch <ver> to change it. The command migrates the API pool (and the GUI FPM pool when installed), recreates panel sockets, and restarts the API worker. Since 5.0.9+, PUT /api/php/default wraps this via the panel API (php-manage ability). Since 5.0.10–5.0.12, switch tolerates partial update-alternatives success, remounts read-only roots when needed, and keeps API/GUI FPM pools on the target version to avoid nginx 502.

REST: GET /api/php, POST /api/php/install, DELETE /api/php/{version}, PUT /api/php/default (API 1.15.0+ / Cipi 5.0.6+). CLI JSON: cipi php list --json (since 5.0.6).

Each app has its own PHP version (set at app create or via cipi app edit --php=). Deployer, Composer, crontab deploy triggers, and cipi sync import always run with the app's configured PHP — never the system default.

To switch an existing app to a different version:

bash
$ cipi app edit myapp --php=8.5

This hot-swaps the PHP version with zero downtime: updates the FPM pool, Nginx socket, Supervisor workers, crontab, Deployer config, and .env in one atomic operation.

cipi ini

Available since v5.1.0. A guided way to change PHP settings without hunting for the right php.ini. Every server-wide change reaches both SAPIs — PHP-FPM and CLI — so what your web requests see is also what queue workers, artisan and cron see.

bash
$ cipi ini list                                # effective values and which layer set them
$ cipi ini list --app=shop                     # as seen by one app
$ cipi ini get memory_limit                    # one setting
$ cipi ini set upload_max_filesize=50M         # server-wide (FPM + CLI)
$ cipi ini set memory_limit=512M --app=shop    # for one app only
$ cipi ini unset memory_limit --app=shop       # fall back to the wider value
$ cipi ini reset [--app=shop]                  # back to Cipi defaults
$ cipi ini keys                                # what can be set, and what cannot

What it does for you

  • It raises the settings that would silently cap yours. Setting upload_max_filesize also raises post_max_size and memory_limit when they would otherwise limit it, and Nginx's client_max_body_size is flagged when it would.
  • It refuses what should not be set this way. Settable keys are an explicit whitelist — see cipi ini keys. open_basedir, auto_prepend_file, extension and friends are refused by name, with the reason, because Cipi manages them as part of app isolation.
  • Per-app overrides win. --app=<app> writes into that app's FPM pool, which outranks the server-wide file — for that app only.

Why this needed fixing

Before 5.1.0 every FPM pool hardcoded upload_max_filesize, post_max_size and max_execution_time, and pool values outrank conf.d — so a server-wide php.ini change could not reach a single app. On top of that, Cipi wrote 99-cipi.ini for FPM only, leaving the CLI SAPI (queue workers, artisan, cron) on the PHP package defaults.

In 5.1.0 pools carry only what is genuinely per-app (open_basedir, the error log, explicit overrides) and inherit everything else; both SAPIs get their file; and migration 5.1.0 backfills the CLI 99-cipi.ini and rewrites existing pools on update.

Each change fires the ini_set notification trigger, so a PHP setting never changes on a shared server without a trace. An app can also carry its PHP version and per-app settings in its repository — see cipi.yml.

cipi db

Cipi creates a dedicated database for each Laravel app automatically during app create. MariaDB (port 3306) is the native default; since v4.8.0 you can also install optional PostgreSQL (port 5432) and choose an engine per database or app. Custom apps do not get a database — use cipi db create --name=<app> if you need one (e.g. for WordPress or another CMS). After setup, a ready-to-use connection URL is displayed (MariaDB: mariadb+ssh://), combining SSH credentials, server IP, and database information for GUI clients like TablePlus, DBeaver, or Sequel Pro.

Engines (v4.8.0+)

bash
$ cipi db install pgsql              # install optional PostgreSQL
$ cipi db uninstall pgsql|mariadb    # remove a non-default engine (destroys data)
$ cipi db default mariadb|pgsql      # server-wide default when --engine is omitted
$ cipi db engines                    # installed engines, ports, and default

Database lifecycle

bash
$ cipi db create                              # interactive
$ cipi db create --name=analytics             # non-interactive (default engine)
$ cipi db create --name=analytics --engine=pgsql
$ cipi db list [--engine=mariadb|pgsql]       # list databases with sizes
$ cipi db backup myapp [--engine=…]
$ cipi db restore myapp backup.sql.gz [--engine=…]
$ cipi db password myapp [--engine=…]         # regenerate password
$ cipi db delete analytics [--engine=…]

Laravel apps store the chosen engine in app metadata; backup, sync, and cipi app reset-db-password follow it. Use cipi app create --engine=pgsql (or the interactive prompt) to provision a Laravel app on PostgreSQL — .env and the connection URL match the engine. Root password reset: cipi reset db-password [--engine=].

cipi alias

Add multiple domains or subdomains to any app. After adding aliases, run cipi ssl install to provision or renew the certificate with SAN coverage for all domains. Since v4.8.0, cipi alias add|remove regenerates the vhost and re-applies SSL with certbot install --redirect so HTTPS is not dropped.

bash
$ cipi alias add myapp www.myapp.com
$ cipi alias add myapp myapp.it
$ cipi alias add myapp '*.myapp.com'   # wildcard (v5.1.0+) — quote it
$ cipi alias list myapp
$ cipi alias remove myapp myapp.it

Since v5.1.0, wildcard aliases such as *.example.com are accepted — Nginx matches a wildcard server_name natively, and multi-tenant apps need it. Quote the pattern so the shell does not expand it, and issue the certificate with cipi ssl install myapp --dns=cloudflare --wildcard: HTTP-01 cannot validate a wildcard.

For www ↔ apex canonical redirects, prefer cipi www instead of managing the counterpart host by hand.

cipi www

Available since v4.8.0. Manage www/apex aliases and canonical 301 redirects for an app. State lives in apps.json (www_redirect); Nginx emits a dedicated redirect server block (ACME path kept public) that survives vhost regeneration. SSL is re-applied via certbot install --redirect.

bash
$ cipi www add myapp              # add counterpart host (www ↔ apex)
$ cipi www force-to-root myapp    # 301 www.domain → domain
$ cipi www force-from-root myapp  # 301 domain → www.domain
$ cipi www clear myapp            # clear redirect state
$ cipi www status myapp           # inspect redirect state

force-to-root / force-from-root auto-add the missing alias when needed.

cipi domains

Available since v4.5.5. A top-level command that lists every domain and alias across all apps in a single table — the global counterpart to the per-app cipi alias list <app>. Ideal for auditing the whole domain-to-app mapping or spotting a domain that is still missing a certificate.

bash
$ cipi domains   # list all domains and aliases across every app

Each row shows the following columns:

Column Description
DOMAIN The domain or alias name. Rows are sorted alphabetically by domain.
APP The app that owns the domain.
KIND primary or alias.
TYPE Laravel or Custom.
PHP The app's PHP version.
DOCROOT public for Laravel apps, /<docroot> for custom apps.
BRANCH The Git branch deployed for the app.
LAST DEPLOY Human-relative age (just now, 10m ago, 2h ago, 3d ago, 2w ago, 2mo ago, 2y ago) — derived from the mtime of the app's current symlink, which Deployer atomically re-points on every successful deploy. Shows - when the app was never deployed.
SSL Per-name certificate status (/), detected from /etc/letsencrypt/live/<domain>.
REPOSITORY The Git repository, or (SFTP only) for custom apps with no repo.
(suffix) When the owning app is suspended, each row ends with ⏸ suspended (yellow). The footer also reports how many apps are suspended.

A footer summarises the totals — number of domains, apps, certificates, and suspended apps — making it easy to audit the whole mapping at a glance.

cipi ssl

Certbot manages Let's Encrypt certificates. Certificates auto-renew via a weekly cron. cipi ssl status shows expiry dates with color-coded warnings: green (>30 days), yellow (14–30 days), red (<14 days).

bash
$ cipi ssl install myapp   # provision / renew — includes all aliases (SAN)
$ cipi ssl force myapp     # re-apply HTTP→HTTPS redirect (no new issuance)
$ cipi ssl renew            # force renewal of all certificates
$ cipi ssl status           # show all certs with expiry dates

# DNS-01 via Cloudflare (v5.0+) — wildcard certs
$ cipi ssl dns set --provider=cloudflare --token=YOUR_CF_TOKEN
$ cipi ssl install myapp --dns=cloudflare
$ cipi ssl install myapp --dns=cloudflare --wildcard

Since v4.8.0, cipi ssl force <app> re-applies the HTTP → HTTPS redirect for an app that already has a Let's Encrypt certificate — without issuing a new one. cipi ssl install also sets force_https in apps.json automatically.

Since v5.0, optional DNS-01 challenges via Cloudflare replace the default HTTP-01 flow when you need wildcard certificates or cannot expose port 80. Configure once with cipi ssl dns set, then pass --dns=cloudflare (and optionally --wildcard) to cipi ssl install.

After adding domain aliases with cipi alias add, always run cipi ssl install again to provision a new SAN certificate covering all domains. If HTTPS redirect was lost after a vhost change, use cipi ssl force instead of re-issuing.

cipi nginx default-server

Available since v5.1.0. Claims whichever of :80 / :443 has no default server and closes unmatched requests with an empty reply (444).

bash
$ cipi nginx default-server status
$ cipi nginx default-server on
$ cipi nginx default-server off

Cipi always had a default server on :80, but never on :443. An HTTPS request carrying an unknown Host therefore fell through to whichever vhost Nginx had loaded first — for a wildcard multi-tenant app, straight into a tenant resolver that cannot parse it.

It is enabled on fresh installs, and migration 5.1.0 claims it on existing servers where nothing else already does. If another vhost legitimately owns the default server, the command says so and changes nothing.

cipi backup

Back up application files and databases to local disk, to Amazon S3, or to any S3-compatible provider (Cloudflare R2, Hetzner Object Storage, DigitalOcean Spaces, Backblaze B2, Scaleway, MinIO, …).

Since v5.1.0 backups are driven by profiles instead of one hardcoded nightly job. A profile answers four questions — what it takes, how often it runs, where it goes and how long it is kept — and profiles are independent, so a 30-minute database-only copy kept on disk sits happily next to an encrypted nightly full copy on S3.

Upgrading from 5.0.x? Migration 5.1.0 converts the old nightly job into a profile named default, carrying over its existing --weeks retention, and takes over the schedule with a managed crontab block. Nothing outside that block is touched, and cipi backup prune <app> --weeks=N still prunes the pre-5.1 layout.

1 — Destinations

cipi backup configure stores the S3 credentials shared by every profile. Since v5.1.0 you may leave the bucket empty for local-only backups, which land under /var/backups/cipi.

bash
$ cipi backup configure
# → AWS Access Key ID
# → AWS Secret Access Key
# → Bucket name        (leave empty for local-only backups)
# → Region
# → Endpoint URL       (leave empty for AWS; required for other providers)

$ cipi backup status                 # destinations, profiles, last run, overdue

S3-compatible endpoints

Provider Endpoint URL
AWS S3 leave empty
Cloudflare R2 https://<account-id>.r2.cloudflarestorage.com
Hetzner https://<datacenter>.your-objectstorage.com
DigitalOcean Spaces https://<region>.digitaloceanspaces.com
Backblaze B2 https://s3.<region>.backblazeb2.com
Scaleway https://s3.<region>.scw.cloud
MinIO https://your-minio-host

2 — Backup profiles

Create a profile once; it then runs on its own schedule. Two typical ones:

bash
# Every 30 minutes — databases only, kept on the server, last 48 runs
$ cipi backup profile add hourly-db --scope=db \
      --databases='shop,tenant_*' --exclude-tables='*.jobs,*.telescope_*' \
      --every=30m --keep=48 --dest=local

# Every night at 02:00 — files + databases, encrypted to S3, kept 14 days
$ cipi backup profile add nightly --scope=all \
      --cron='0 2 * * *' --keep-days=14 --dest=s3 --encrypt
bash
$ cipi backup profile list                 # all profiles
$ cipi backup profile show nightly         # one profile in detail
$ cipi backup profile edit nightly --keep-days=30
$ cipi backup profile disable hourly-db    # pause it without deleting it
$ cipi backup profile enable hourly-db
$ cipi backup profile remove hourly-db     # deletes the profile, keeps its archives

Flags for profile add / profile edit

--scope=all|files|dbWhat the profile takes: application files, databases, or both.
--apps='shop,blog'Which apps its file archives cover. Glob patterns allowed.
--databases='main,tenant_*'Which databases it covers. They are discovered from the engine itself, so tenant databases an app creates at runtime are matched too.
--exclude-databases='<glob,…>'Databases to leave out of an otherwise broad selection.
--exclude-tables='*.jobs,*.telescope_*'Tables to skip — expanded against the live table list on MariaDB, passed straight to pg_dump on PostgreSQL.
--every=30m|6h|1d or --cron='0 2 * * *'How often it runs.
--dest=local|s3|local,s3Where the run goes.
--keep=N / --keep-days=N / --keep-weeks=NRetention — at least one is mandatory. A profile that would grow without bound is refused.
--encrypt / --no-encryptClient-side AES-256 encryption before anything leaves the server.
Multi-tenant apps are covered now. Before 5.1.0 a run dumped exactly one database per app — the one named after the app — so tenant_1, tenant_2, … were never saved at all. Databases now come from the engine (minus system schemas) and are selected with glob patterns.

3 — Encryption

With --encrypt the archive is encrypted with AES-256 on the server, before it is uploaded, so the bucket operator never holds readable data. The manifest stays readable so a run can still be identified.

bash
$ cipi backup key show      # print the key — store it off-server
$ cipi backup key rotate    # new key for future runs
Without the key an encrypted backup cannot be restored — not by you, not by Cipi. Save it in a password manager the moment you enable --encrypt, and remember that runs taken before a key rotate still need the old key.

4 — Running, checking and restoring

bash
$ cipi backup run                          # run every enabled profile now
$ cipi backup run --profile=nightly        # run one profile now
$ cipi backup run --dry-run                # show what it would take, take nothing
$ cipi backup list [--profile=nightly]     # runs held on each destination
$ cipi backup verify                       # does the newest run of each profile open?
$ cipi backup verify --deep                # same, downloading from S3 to check
$ cipi backup prune [--profile=nightly]    # apply retention now
$ cipi backup fetch nightly 2026-09-02_020000   # download and decrypt one run

Since v5.1.0 these commands tell you the truth rather than reassuring you:

  • Every archive is integrity-checked before it ships. A dump cut short by a full disk is still a non-empty file, so the old size check passed it; now a corrupt archive is discarded and reported instead of quietly replacing a good backup.
  • cipi backup fetch no longer reports success on a prefix that holds no objects, so a mistyped timestamp is an error rather than an empty directory.
  • cipi backup verify says plainly that there is nothing to verify when no run has happened yet, instead of “passed”.
  • tar warnings — “file changed as we read it”, constant on a live app writing its logs — no longer fail a perfectly good archive. Only exit code 2 and above counts.
  • A disabled profile actually stops running, and Encrypted is displayed correctly.

To restore, fetch the run and feed the pieces back:

bash
$ cipi backup fetch nightly 2026-09-02_020000 --dest=/root/restore
$ cipi db restore myapp /root/restore/databases/mariadb/myapp.sql.gz
$ tar -tzf /root/restore/apps/myapp/files.tar.gz

5 — Storage layout

Application files and databases are stored separately, with a manifest per run — so a database-only profile costs nothing to take, and a database can be restored without unpacking an app.

text
<root>/<profile>/<YYYY-MM-DD_HHMMSS>/
├── manifest.json
├── apps/
│   └── myapp/
│       ├── files.tar.gz
│       └── meta.json
└── databases/
    └── mariadb/
        └── myapp.sql.gz

# local  → /var/backups/cipi/<profile>/<timestamp>/
# s3     → s3://<bucket>/cipi/<profile>/<timestamp>/

Backup staging defaults to /var/tmp (disk) rather than /tmp (often a small RAM-backed tmpfs), so large apps do not fail mid-backup when tmpfs fills up. Override with tmpdir in backup.json (set via cipi backup configure) or the CIPI_BACKUP_TMPDIR environment variable.

6 — Schedule and the staleness watchdog

You no longer write cron lines by hand. cipi backup configure and every profile change rewrite a marked block in root's crontab; anything outside that block is left alone, and pre-5.1 hand-written lines are absorbed on migration.

An hourly watchdog raises the backup_stale notification when a profile has not succeeded within twice its own interval — because a backup that quietly stopped running is worse than no backup at all: it still looks configured. A freshly created profile is not reported as overdue before its first scheduled run could have happened.

Legacy pruning

cipi backup prune <app> --weeks=N keeps working against the pre-5.1 layout (s3://<bucket>/cipi/<app>/<timestamp>/ plus pre-deploy dumps in /var/log/cipi/backups), so existing archives and any hand-written cron line still prune. New profiles use --keep, --keep-days or --keep-weeks instead.

An app can declare its own backup profiles in its repository — see cipi.yml. Profiles owned by an app must be named <app> or <app>-*; server-wide profiles stay under your control only.

User crontab

Cipi automatically adds a crontab entry for the Laravel scheduler when an app is created:

bash
# installed automatically by cipi app create
* * * * * /usr/bin/php8.5 /home/myapp/current/artisan schedule:run >> /dev/null 2>&1

This entry runs as the myapp Linux user every minute, using the PHP version selected for the app. It is updated automatically when you change PHP version via cipi app edit myapp --php=X.

Viewing the current crontab

bash
# as root — view the app user's crontab
$ crontab -u myapp -l

# or after switching to the app user
$ su - myapp
myapp@server:~$ crontab -l

Adding custom cron jobs

You can add extra cron jobs to the app user's crontab. Switch to the app user first to ensure jobs run with the correct user context and file permissions:

bash
$ su - myapp
myapp@server:~$ crontab -e

Example entries you might add:

bash
# existing Laravel scheduler (do not remove)
* * * * * /usr/bin/php8.5 /home/myapp/current/artisan schedule:run >> /dev/null 2>&1

# nightly database backup at 2 AM
0 2 * * * /usr/local/bin/cipi db backup myapp >> /home/myapp/logs/backup.log 2>&1

# custom script every 15 minutes
*/15 * * * * /home/myapp/current/scripts/sync.sh >> /home/myapp/logs/sync.log 2>&1
Do not remove the Laravel scheduler entry. Cipi does not re-add it automatically if deleted — you would need to run cipi app edit myapp --php=<current-version> to restore it. Always keep the schedule:run line as the first entry so it is easy to identify.
Cron jobs run as the app user. They respect the same filesystem restrictions as the app itself. If a cron job needs to write files, make sure the target path is inside /home/myapp/. Jobs that require root access should be added to the root crontab instead, with crontab -e as root.

Checking if cron is working

bash
# check system cron log
$ grep CRON /var/log/syslog | grep myapp | tail -20

# check Laravel scheduler execution
$ cipi app artisan myapp schedule:list

cipi schedule

Since v5.0, manage the Laravel scheduler crontab entry that runs schedule:run (the crontab itself already existed; it is now toggleable and tracked in apps.json).

bash
$ cipi schedule on myapp
$ cipi schedule off myapp
$ cipi schedule status myapp

cipi worker & Laravel Horizon

Every app gets a default Supervisor worker for the default queue. You can add additional queues with custom process counts and timeouts.

bash
$ cipi worker add myapp --queue=emails --processes=3
$ cipi worker add myapp --queue=exports --processes=1 --timeout=7200
$ cipi worker list myapp
$ cipi worker edit myapp --queue=default --processes=3
$ cipi worker remove myapp emails
$ cipi worker restart myapp   # restart all workers for the app
$ cipi worker stop myapp      # stop all workers for the app (used during deploys)

# Laravel Horizon (v5.0+) — mutually exclusive with queue:work workers
$ cipi worker horizon enable myapp
$ cipi worker horizon status myapp
$ cipi worker horizon disable myapp

With Horizon enabled, deploy runs horizon:terminate and restarts workers via cipi-worker. You cannot run classic queue:work workers and Horizon on the same app at once.

On 5.0.x, cipi worker horizon enable could leave Horizon half enabled: the command printed nothing after “Enabling Horizon…” and status still said disabled. v5.1.0 fixes both causes, always writes the state, reports what Supervisor actually did, and makes status surface any drift between the two. If you hit this, run cipi self-update and enable it again.

Queue workers can also be declared in the app's repository — see cipi.yml.

Flag Description
--queue=<name> Queue name to consume (e.g. default, emails, exports)
--processes=<n> Number of parallel worker processes
--timeout=<seconds> Job timeout in seconds. Default is 60.

Workers are stopped before the symlink swap and restarted after every deploy, preventing Supervisor from picking up stale artisan paths. Supervisor is configured with autorestart=unexpected so workers only restart on unexpected exits, not on graceful stops.

App-user helper: cipi-worker

Each app user can restart, stop, or check workers without root — via a restricted sudo helper installed at /usr/local/bin/cipi-worker:

bash
# run as the app user (SSH or sudo su - myapp)
$ sudo cipi-worker status myapp
$ sudo cipi-worker stop myapp      # used by Deployer before symlink swap
$ sudo cipi-worker restart myapp

As root, use cipi worker list|restart|stop <app> instead. There is no cipi worker status admin command — use cipi worker list or the app-user helper above.

cipi health

Since v5.0, configure HTTP healthchecks per app. Cipi then watches the app in two different ways, because “is the site up?” and “did the push I just made break production?” are not the same question.

Check When it runs When it alerts
Periodic probe Every 5 minutes After 3 consecutive failures → health_fail
Post-deploy check Right after every release goes live Immediately on the first failure → deploy_health_fail
bash
$ cipi health set myapp --url=https://myapp.com/up --expect=200
$ cipi health check myapp
$ cipi health list
$ cipi health unset myapp

# Structured output (v5.0.6+) — panel API / scripts
$ cipi health list --json
$ cipi health check myapp --json

Post-deploy verification (v5.1.0+)

Once an app has a healthcheck URL, the release that just went live is verified right after every deploy — from cipi deploy and from the Git webhook alike. The probe waits a short grace period (8 seconds for Octane apps, 3 otherwise, or whatever --grace=N says) and retries five times, so an app that needs a moment to come up is not reported as broken.

The verdict never changes the deploy's exit code — the release is live either way — but the alert says so and hands you the rollback command. The success email is sent after verification, so it can never announce a successful deploy while the site is returning 500.

bash
$ cipi health postdeploy myapp             # run the post-deploy verification now
$ cipi health set myapp --grace=15         # give the release longer to warm up (max 120)
$ cipi health set myapp --no-postdeploy    # turn the post-deploy check off for this app

Auto-rollback of an unhealthy release (opt-in)

Cipi can undo a release that fails its post-deploy healthcheck: the current symlink moves back to the previous release, the app is probed again, and one email describes the whole sequence — what was published, what it answered, what it was rolled back to and whether that fixed it.

bash
$ cipi health set myapp --rollback-on-unhealthy   # permanent, per app
$ cipi deploy myapp --rollback-on-unhealthy       # just this deploy

Four outcomes are reported distinctly: recovered; rolled back but still unhealthy (so the cause is probably not the code); the rollback itself failed (the bad release is still live); and there is no earlier release to return to.

Auto-rollback is off by default and deliberately so: database migrations are not undone. A release that migrated the schema and then failed can be worse off after the code is rolled back. Enable it only for apps whose deploys do not migrate, or where you know the migrations are backwards-compatible.

REST: GET /api/health, GET|PUT|DELETE /api/apps/{name}/health, POST /api/apps/{name}/health/check (API 1.15.0+ / Cipi 5.0.6+). An app can also declare its healthcheck in its repository — see cipi.yml.

cipi firewall

Cipi installs UFW with ports 22, 80, and 443 open by default. Use the firewall commands to manage additional rules without touching UFW directly.

bash
$ cipi firewall allow 3306                  # open a port
$ cipi firewall allow 3306 --from=10.0.0.5  # allow from specific IP
$ cipi firewall allow 3306 --from=10.0.0.0/24 # allow from subnet
$ cipi firewall deny 8080                   # block a port
$ cipi firewall list                        # show all rules

cipi ban

Inspect and manage Fail2ban bans directly from the CLI. Cipi configures Fail2ban with progressive banning: a 24-hour base ban that doubles on each repeat offence up to a 7-day cap, with max retries reduced to 3. A dedicated recidive jail bans repeat offenders for 7 days after 3 bans within 24 hours.

bash
$ cipi ban list                        # list all banned IPs, grouped by jail
$ cipi ban unban 203.0.113.42           # unban a specific IP from all jails

cipi ban list

Lists every IP currently banned by Fail2ban, grouped by jail (e.g. sshd, recidive). Useful for a quick security check or before running an unban.

cipi ban unban <IP>

Removes the given IP from all Fail2ban jails at once. Handy when a legitimate user or CI runner gets locked out by mistake.

Existing installations are upgraded automatically by the 4.3.0 migration script when you run cipi self-update. No manual configuration is needed.

cipi service

Check and control the system services that power Cipi directly from the CLI. Nginx uses a graceful reload (zero downtime) instead of a full restart.

bash
$ cipi service list                    # status of all services
$ cipi service list nginx              # status of a specific service
$ cipi service restart                 # restart all services
$ cipi service restart nginx           # graceful reload (zero downtime)
$ cipi service restart php             # restart all PHP-FPM versions
$ cipi service start fail2ban
$ cipi service stop supervisor         # asks for confirmation

# Structured output (v5.0.6+) — panel API
$ cipi service list --json

REST: GET /api/services, POST /api/services/{name}/restart (API 1.15.0+ / Cipi 5.0.6+; abilities services-view / services-manage).

Supported service names: nginx, mariadb, postgresql (aliases: pgsql, postgres — when installed via cipi db install pgsql), valkey-server, supervisor, fail2ban, php<ver>-fpm (e.g. php8.5-fpm). The keyword php targets all installed PHP-FPM versions at once. For the cache backend, redis-server, redis, and valkey are accepted as aliases of valkey-server.

Valkey — the BSD-licensed, Redis-compatible fork — is included in the default stack and replaces redis-server since Cipi 4.5.6. It is installed with a password, bound to localhost only, and its credentials (user, password) are saved in /etc/cipi/server.json and shown at the end of installation. valkey-server is added to the unattended-upgrades blacklist — Cipi manages it, so it is not auto-upgraded automatically. See the Valkey section for details and the automatic Redis → Valkey migration.

cipi ssh — SSH Key Management

Manage the authorized SSH keys for the cipi user — the admin SSH entry point. The cipi user (group cipi-ssh) uses public-key only; root login is disabled. App users (group cipi-apps) connect with password — see SSH as the app user.

Commands

bash
$ cipi ssh list                 # list all authorized keys with fingerprint, comment, and current-session marker
$ cipi ssh add [key]             # add a new SSH public key (validates format, prevents duplicates)
$ cipi ssh remove [n]            # remove a key by number
$ cipi ssh rename [n] [name]     # change the display name / comment of a key

# Structured output (v5.0.6+) — panel API
$ cipi ssh list --json

REST: GET|POST /api/ssh/keys, DELETE /api/ssh/keys/{n} (API 1.15.0+ / Cipi 5.0.6+).

Safety mechanisms

cipi ssh remove includes two safeguards to prevent lockout:

  • Current-session protection — you cannot remove the key used by your active SSH session.
  • Last-key protection — you cannot remove the last remaining authorized key.

Key comments

SSH keys are stored with their original comments intact, making it easy to identify who each key belongs to. Use cipi ssh rename to change the display name of any key:

bash
# list keys to find the number
$ cipi ssh list

# rename key #2
$ cipi ssh rename 2 "john-macbook"

Email notifications

When SMTP is configured, Cipi sends an email alert every time a key is added, removed, or renamed. The notification includes the server hostname, IP address, key fingerprint, key comment, timestamp, and remaining key count. Rename notifications also include the old and new key name.

cipi — Server & Self-Update

Top-level commands for server status and Cipi self-management.

bash
$ cipi status              # CPU, RAM, disk, services, PHP versions, apps
$ cipi version             # show installed Cipi version
$ cipi self-update         # update Cipi to the latest version
$ cipi self-update --check # check for updates without installing

Password & credential reset

Cipi provides commands to regenerate server-level passwords. New passwords are stored in /etc/cipi/server.json (encrypted via Vault) and displayed on screen. Save them immediately — they are shown only once.

bash
$ cipi reset root-password              # regenerate the root Linux user SSH password
$ cipi reset db-password [--engine=…]   # regenerate root password (default or chosen engine)
$ cipi reset valkey-password           # regenerate the Valkey password and restart the service
cipi reset valkey-password (alias cipi reset redis-password) restarts the Valkey service. Connected clients will be temporarily disconnected. If your apps use Valkey for cache or sessions, expect a brief interruption.