Manage Laravel apps with cipi.yml
By Andrea Pollastri · Last updated: · free to read, no paywall
The state an app expects on the server — aliases, PHP, workers, health, backups — usually lives in a panel or in someone's head. Since Cipi 5.1 it can live in a cipi.yml at the root of the repository, reviewed in the same pull request as the code that depends on it. This guide is the practical loop: generate, plan, apply, then opt in so every deploy reconciles.
Why the file belongs next to the code
A release that adds a queued job without a worker is half-shipped. A rollback that leaves last week's upload_max_filesize is half-rolled-back. 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 the Laravel application makes that state part of the same pull request as the code that depends on 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 product page cipi.yml — config that ships with the code is the overview. The full schema lives in Docs → Deploy → cipi.yml. This guide is the day-to-day loop for one app.
Start from the live server
You do not have to write the file by hand. On a Cipi 5.1+ box, print the app's current configuration — aliases, PHP version and per-app settings, extra databases, queue workers read back out of Supervisor, Horizon, Reverb, the scheduler and the backup profiles the app owns — as a ready-to-commit file:
$ cipi yml generate myapp > cipi.yml
$ cipi yml plan myapp # reports nothing to do
Commit that file at the repository root. Cipi looks it up in current/cipi.yml, then current/cipi.yaml, then shared/cipi.yml — override with --file=<path> if you keep it elsewhere.
Prefer a blank, fully commented template? cipi yml example myapp prints one with placeholder databases and profiles already inside that app's namespace, so the template validates as-is.
The commands you will use
| Command | What it does |
|---|---|
cipi yml generate |
Prints the app's configuration as it stands on the server, ready to commit |
cipi yml example |
A blank commented template, namespaced to the app so it 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 |
cipi yml apply |
Applies the plan. Add --yes for scripts and CI |
cipi yml auto |
on / off / status — reconcile after every successful deploy |
$ cipi yml generate myapp
$ cipi yml example myapp
$ cipi yml validate myapp
$ cipi yml plan myapp
$ cipi yml apply myapp [--yes]
$ cipi yml auto myapp on|off|status
What you can declare
Seven areas. You do not have to declare all of them — only the sections you write are reconciled.
- Aliases — the declared list replaces the current one. An alias you delete from the file is removed from Nginx. Wildcards such as
*.myapp.comare accepted. The primary domain is never managed here. - PHP 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_limitand the rest. Server-wide values stay withcipi ini set. - Extra databases — beyond the one created with the app, on MariaDB or PostgreSQL. Credentials land in
shared/cipi-databases.envand are never written back to the repository. Databases are created, never dropped. - Queue workers and Horizon — declare each queue with process count, tries and timeout, and Supervisor is reconciled to match. Or set
horizon: trueand let Horizon own the queues. Since 5.1.2,horizon: falseis actually read — previouslyfalsewas treated like a missing key. - Laravel Reverb — since 5.1.2,
reverb: truegives the app a localhost port, a Supervisor program, an nginx proxy for/app/{key}and/apps/{id}/…on its own domain, and generatedREVERB_*credentials. Laravel apps only. An app that already owns/appor/appscannot share that domain with Reverb. - Scheduler — one boolean turns
* * * * * artisan schedule:runon or off. No crontab editing on the next server. - Healthcheck — 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.
- Backup profiles — per-app profiles with their own scope, schedule, retention, destinations and encryption. Names must be
myappormyapp-*.
A complete file
This is what a production app typically ships. You can generate most of it; the comments are for the pull request.
version: 1
app:
php: "8.5"
aliases:
- "www.myapp.com"
- "*.myapp.com"
ini:
upload_max_filesize: 50M
post_max_size: 60M
memory_limit: 512M
databases:
- name: myapp_reporting
- name: myapp_analytics
engine: pgsql
workers:
horizon: false
reverb: false
queues:
- queue: default
processes: 2
- queue: emails
processes: 1
tries: 5
timeout: 300
schedule: true
health:
url: "https://myapp.com/up"
expect: 200
backup:
profiles:
- name: myapp-db
scope: db
databases: ["myapp", "myapp_*"]
exclude_tables: ["*.jobs", "*.telescope_*"]
every: 30m
keep: 48
destinations: [local]
- name: myapp-nightly
scope: all
cron: "0 2 * * *"
keep_days: 14
destinations: [s3]
encrypt: true
Plan, then apply
Nothing changes until you look at the diff. cipi yml plan myapp lists every alias, worker, database, setting and profile that would be added, changed or removed. Read it the way you read a pull request.
$ cipi yml validate myapp
$ cipi yml plan myapp
$ cipi yml apply myapp # asks for confirmation
$ cipi yml apply myapp --yes # scripts and CI
A file that fails validation is rejected as a whole and never partially applied. That is the same fail-closed path a deploy uses when yml auto is on: email yml_fail, no half-state on the box.
A week of real changes
Treat the file like application code. Each of these is a one-line (or one-block) commit, reviewed, then applied — or applied automatically after the release if yml auto is already on.
- Monday — aliases. Add
www.myapp.comand*.myapp.comfor multi-tenant subdomains. Remember: the list is authoritative. Dropping an alias from the file removes it from Nginx. - Tuesday — php.ini. A form starts accepting 40 MB uploads. Bump
upload_max_filesizeandpost_max_sizeinapp.ini. Server-wide defaults stay withcipi ini set. - Wednesday — a new queue. The release adds an
emailsqueue. Add the worker withprocesses,triesandtimeout. Supervisor matches the file. If you switch the app to Horizon, sethorizon: trueand drop the queue list. - Thursday — an extra database. Reporting needs its own MariaDB (or
engine: pgsql). Name itmyapp_reporting. Credentials appear in/home/myapp/shared/cipi-databases.env— never in Git. Cipi creates databases; it will not drop them if you delete the entry later. - Friday — health and backups. Point
health.urlathttps://myapp.com/up(it must be one of the app's own domains). Add a cheapmyapp-dbprofile every 30 minutes and an encrypted nightly copy to S3.
PHP 8.5 must already be installed (cipi php install 8.5) before you pin app.php to it. The file will not install a runtime that is not on the box.
Opt in after every deploy
Deploys ignore the file until you say otherwise. That is deliberate: a first commit of cipi.yml should not surprise production.
$ cipi yml auto myapp on # reconcile after every successful deploy
$ cipi yml auto myapp status
$ cipi yml auto myapp off # back to manual apply
With the 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. A file that fails validation is reported by email (yml_fail) and never applied halfway. A successful reconcile fires yml_apply.
What the file will not touch
- It can only configure an app that already exists — never create, rename or delete one.
- Sections you omit are left alone. The one deliberate exception is the alias list: it is authoritative.
- The app's own primary database and server-wide backup profiles stay yours.
generateleaves them out on purpose. - Databases are created, never dropped. Removing a database entry from the file does not delete the database.
- No field carries a shell command or a path to include. Unknown keys are errors.
Safe to accept over Git
The file arrives from a repository, so anyone who can commit controls its contents. The schema is fail-closed throughout:
- Databases must be named
<app>or<app>_*; backup profiles<app>or<app>-*. - The healthcheck URL must resolve to one of the app's own domains — otherwise a commit could aim the five-minute prober at an internal address and read the answer back out of the alert emails.
- The parser implements a small YAML subset and refuses anchors, aliases, tags, merge keys, block scalars and flow mappings outright.
Once 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.
Ship the file with the next release
Generate what the server already has, commit it, read the plan, then turn yml auto on when you trust the loop. The overview and the full schema are one click away.
Frequently asked questions
Do I have to write cipi.yml by hand?
No. cipi yml generate <app> prints the app's current server configuration as a ready-to-commit file, and cipi yml example prints a blank commented template if you would rather start from scratch.
Does a deploy apply the file automatically?
Only after you opt in with cipi yml auto <app> on. Until then deploys ignore the file and you reconcile manually with plan and apply.
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.
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 yml auto on.
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.