Infrastructure
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).
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>.$ 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, restarts the API worker, and sends an email
notification
if SMTP is configured.
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:
$ 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 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+)
$ 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
$ 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.
$ cipi alias add myapp www.myapp.com $ cipi alias add myapp myapp.it $ cipi alias list myapp $ cipi alias remove myapp myapp.it
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.
$ 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.
$ 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).
$ 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.
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 backup
Back up databases and storage to Amazon S3 or any S3-compatible provider (Hetzner Object Storage, DigitalOcean Spaces, Backblaze B2, MinIO, etc.).
Setup
$ cipi backup configure # → AWS Access Key ID # → AWS Secret Access Key # → Bucket name # → Region # → Endpoint URL (leave empty for AWS; required for other providers)
S3-compatible endpoints
| Provider | Endpoint URL |
|---|---|
| AWS S3 | leave empty |
| Hetzner | https://<datacenter>.your-objectstorage.com |
| DigitalOcean Spaces | https://<region>.digitaloceanspaces.com |
| Backblaze B2 | https://s3.<region>.backblazeb2.com |
| MinIO | https://your-minio-host |
Running backups
$ cipi backup configure # configure S3 credentials $ cipi backup run # backup all apps $ cipi backup run myapp # backup a single app $ cipi backup list # list all backups $ cipi backup list myapp # list backups for one app $ cipi backup prune myapp --weeks=4 # delete backups older than 4 weeks
Each backup uploads to s3://your-bucket/cipi/appname/YYYY-MM-DD_HHMMSS/ and contains:
db.sql.gz— compressed database dumpshared.tar.gz— the entireshared/directory (.env+storage/)
Since v4.7.14, backup staging defaults to /var/tmp (disk) instead of
/tmp (often a small RAM-backed tmpfs), so large apps no longer 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.
Scheduling automatic backups
# Add to root crontab (crontab -e)
0 2 * * * /usr/local/bin/cipi backup run >> /var/log/cipi/backup.log 2>&1
Pruning old backups from S3
Backups accumulate over time. Use cipi backup prune to delete backup folders older than
N weeks from S3. Run it as a cron job alongside the backup itself.
$ cipi backup prune myapp --weeks=4 # delete backups older than 4 weeks $ cipi backup prune myapp --weeks=2 # keep only the last 2 weeks
Add both commands to the root crontab to run automatically:
# root crontab — backup at 02:00, prune at 03:00 (keep 4 weeks)
0 2 * * * /usr/local/bin/cipi backup run myapp >> /var/log/cipi/backup.log 2>&1
0 3 * * * /usr/local/bin/cipi backup prune myapp --weeks=4 >> /var/log/cipi/backup-prune.log 2>&1
cipi backup prune reads S3 credentials from /etc/cipi/backup.conf,
written by cipi backup configure. It works with any S3-compatible provider.
Adjust --weeks to match your retention policy (e.g. --weeks=2 for two
weeks, --weeks=8 for two months).
User crontab
Cipi automatically adds a crontab entry for the Laravel scheduler when an app is created:
# 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
# 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:
$ su - myapp
myapp@server:~$ crontab -e
Example entries you might add:
# 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
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.
/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
# 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).
$ 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.
$ 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.
| 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:
# 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. A cron runs every 5 minutes; after
3 consecutive failures Cipi notifies via the health_fail trigger (SMTP when
configured).
$ cipi health set myapp --url=https://myapp.com/up --expect=200 $ cipi health check myapp $ cipi health list $ cipi health unset myapp
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.
$ 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.
$ 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.
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.
$ 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
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
$ 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
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:
# 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.
$ 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.
$ 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.