cipi.yml — config that ships with the code.
One file at the root of your repository describes the state an app expects on the
server: domain aliases, PHP version and per-app settings, extra databases, queue workers, the
scheduler, its healthcheck and its backup profiles. Review it in a pull request, ship it with the
release, and let cipi yml apply reconcile the server with it.
Server configuration, reviewed like code
Server state usually lives somewhere else: a panel, a wiki page, or the memory of whoever set the box up.
A cipi.yml committed next to your Laravel application makes that state part of the same pull
request as the code that depends on it. A release that adds a queue also adds its worker. A release that
needs a bigger upload_max_filesize carries it. Roll the code back, and the configuration rolls
back with it.
The file is declarative: it describes the end state, not the steps. Cipi reads what the server has, compares it with the file and shows you the difference before touching anything.
The commands
$ cipi yml generate myapp # this app's current config, as a cipi.yml $ cipi yml example myapp # blank commented template, in myapp's namespace $ cipi yml validate myapp # parse and check, change nothing $ cipi yml plan myapp # show exactly what would change $ cipi yml apply myapp [--yes] # apply it $ cipi yml auto myapp on|off|status # apply after every successful deploy
| Command | What it does |
|---|---|
cipi yml generate |
Prints the app's configuration as it stands on the server — aliases, PHP version and per-app settings, extra databases, queue workers read back out of Supervisor, the scheduler and the backup profiles the app owns — as a ready-to-commit file. |
cipi yml example |
A blank, fully commented template. Pass an app name and the placeholder databases and profiles land inside that app's namespace, so the template validates as-is. |
cipi yml validate |
Parses the file and checks every value against the schema. Changes nothing. |
cipi yml plan |
The diff: every alias, worker, database, setting and profile that would be added, changed or removed. Read this before you apply. |
cipi yml apply |
Applies the plan. Add --yes for scripts and CI. |
cipi yml auto |
Opt in — or out — of reconciling automatically after every successful deploy.
status reports the current setting. |
The file is looked up in current/cipi.yml, then current/cipi.yaml, then
shared/cipi.yml — override with --file=<path>.
What the file can configure
Domain aliases
The declared list replaces the current one — an alias you delete from the file is removed from
Nginx. Wildcards such as *.myapp.com are accepted, for multi-tenant subdomains. The
primary domain is never managed here.
PHP version and php.ini
Pin the app to PHP 8.3, 8.4 or 8.5 (already installed on the server) and set per-app overrides —
upload_max_filesize, post_max_size, memory_limit and the
rest. Server-wide values stay with cipi ini set.
Extra databases
Beyond the one created with the app: reporting, analytics, whatever the release needs, on MariaDB or
PostgreSQL. Credentials land in shared/cipi-databases.env and are never written back
to the repository. Databases are created, never dropped.
Queue workers and Horizon
Declare each queue with its process count, tries and timeout, and Supervisor is reconciled to
match. Or set horizon: true and let Horizon own the queues instead.
The Laravel scheduler
One boolean turns * * * * * artisan schedule:run on or off for the app. No crontab
editing, no forgotten entry on the next server.
Healthcheck and rollback
An HTTP probe on one of the app's own domains, checked every five minutes and again right after every deploy. Optionally roll a failing release back automatically — the code symlink only, migrations are not undone.
Health checks in the docs → backup.profilesBackup profiles
Per-app profiles with their own scope, schedule, retention, destinations and encryption — a cheap database run every 30 minutes, a complete encrypted copy to S3 at night. Profile names are scoped to the app.
Backups in the docs → referenceEvery key, documented
The full schema, the lookup order, the validation rules and the notification hooks live in the deploy chapter of the documentation.
Read the reference →A complete file
You do not have to write it by hand. cipi yml generate <app> prints what the server
already has; commit that and cipi yml plan reports nothing to do.
$ cipi yml generate myapp > cipi.yml # then commit it $ cipi yml plan myapp # reports nothing to do
version: 1 app: # 8.3, 8.4 or 8.5 — must already be installed (cipi php install 8.5) php: "8.5" # The declared list replaces the current aliases: one you remove here is # removed from the server. The primary domain is not managed here. aliases: - "www.myapp.com" - "*.myapp.com" # wildcard, for multi-tenant subdomains # Per-app php.ini overrides. Server-wide values stay with `cipi ini set`. ini: upload_max_filesize: 50M post_max_size: 60M memory_limit: 512M # Extra databases beyond the one created with the app. Credentials land in # /home/myapp/shared/cipi-databases.env — never written back to the repo. databases: - name: myapp_reporting - name: myapp_analytics engine: pgsql # mariadb (default) or pgsql workers: horizon: false # true replaces the queue workers below queues: - queue: default processes: 2 - queue: emails processes: 1 tries: 5 timeout: 300 # Laravel scheduler (* * * * * artisan schedule:run) schedule: true # HTTP healthcheck. Probed every 5 minutes and right after every deploy. # The URL must be one of this app's own domains. health: url: "https://myapp.com/up" expect: 200 # grace: 8 # seconds before the first probe after a deploy # postdeploy: false # skip the check right after a deploy # rollback_on_unhealthy: true # undo a release that fails the check # # (the code symlink only — migrations are NOT undone) # Backup strategy for this app. Profile names must be myapp or myapp-*. backup: profiles: # Frequent and cheap: databases only, without the noisy tables. - name: myapp-db scope: db databases: ["myapp", "myapp_*", "tenant_*"] exclude_tables: ["*.jobs", "*.telescope_*"] every: 30m # 5m/10m/15m/20m/30m, 1h..12h, 1d..28d keep: 48 # keep the last 48 runs destinations: [local] # Slower, complete, off-site and encrypted. - name: myapp-nightly scope: all # all | files | db cron: "0 2 * * *" keep_days: 14 destinations: [s3] encrypt: true
Deploys ignore the file until you opt in
Nothing happens on deploy until you run cipi yml auto <app> on. With that opt-in given,
every successful deploy reconciles — from both cipi deploy and the Git
webhook. A release that carries no cipi.yml is a quiet no-op, and a file that fails validation
is reported by email (yml_fail) and never partially applied. A successful
reconcile fires yml_apply.
$ cipi yml auto myapp on # reconcile after every successful deploy $ cipi yml auto myapp status # is it on? $ cipi yml auto myapp off # back to manual apply
Why it is safe to accept over Git
The file arrives from a repository, so anyone who can commit controls its contents. It is fail-closed throughout:
- It can only configure an app that already exists — never create, rename or delete one.
- Its databases must be named
<app>or<app>_*, and its backup profiles<app>or<app>-*. - Its healthcheck URL must resolve to one of the app's own domains — otherwise a commit could aim the server's prober at an internal address and read the answer back out of the alert emails.
- Unknown keys are errors, and no field carries a shell command or a path to include.
- The parser implements a deliberately small YAML subset and refuses anchors, aliases, tags, merge keys, block scalars and flow mappings outright.
yml auto is on, anyone who can push to that repository can change the app's aliases,
PHP settings, workers, healthcheck and backup profiles. That is the point of configuration-as-code —
treat write access to the repo accordingly.Questions
Do I have to write the file by hand?
No. cipi yml generate <app> prints the app's current server configuration as a
ready-to-commit cipi.yml, and cipi yml example prints a blank commented
template if you would rather start from scratch.
Can a commit break my server?
The schema carries no shell commands and no include paths, an app can only touch its own databases and
backup profiles, and a file that fails validation is rejected as a whole rather than applied halfway.
Deploys ignore the file entirely until you turn cipi yml auto on.
What happens to things the file does not mention?
They are left alone. Only the sections you declare are reconciled — with one deliberate exception: the alias list is authoritative, so removing an alias from the file removes it from the server.
Does it work with the Git webhook?
Yes. With cipi yml auto on, both cipi deploy and a webhook-triggered deploy
reconcile after a successful release.