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 — and since 5.4 its redirects,
proxies, basic auth, crons, recipe options and a Node app's build. 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 $ cipi yml post-deploy myapp # run the declared deploy.post steps now (5.2.3+)
| 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, the backup profiles the app owns and, since 5.4.0, the new keys below (basic auth user names only, never hashes) — 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. |
cipi yml post-deploy (5.2.3) |
Runs the declared deploy.post steps now, against current/ — for
testing them without pushing a commit. |
The file is looked up in current/cipi.yml, then current/cipi.yaml, then
shared/cipi.yml — and, since 5.2.3, in htdocs/, where a custom
app's repository lives. Override with --file=<path>. Since 5.2.3 every app vhost also
denies /cipi.yml and /cipi.yaml, so the file is never downloadable from the
site it configures.
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. Since
5.1.2, horizon: false and reverb: false are actually
read — previously false was treated like a missing key, so declaring
horizon: false on an app running Horizon did nothing.
WebSockets with Reverb
Since 5.1.2, reverb: true gives the app a Laravel Reverb server — a
localhost port, a Supervisor program, an nginx proxy for /app and /apps on
its own domain, and generated REVERB_* credentials. Laravel apps only.
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. enabled: false removes the check — ignored before
5.3.0, honoured since.
Backup 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 → deploy.postPost-deploy steps
New in 5.2.3. A declared list of allowlisted commands — artisan, npm,
composer, a php or node script — run from the live release
after every successful deploy. No free shell, and the one section that does not need
yml auto.
www redirect and basic auth
New in 5.4.0. to-root, from-root or none, checked against the
alias list at plan time. Basic auth users by name — keeping the server's password — or with a bcrypt
/ SHA-512 hash. Never a password in the repository.
Redirects and proxy prefixes
New in 5.4.0. The whole-app redirect, path redirects and reverse proxies, validated as one set and
applied as one nginx -t. Stricter than the CLI: no --force, no Cipi ports,
no link-local or cloud-metadata upstreams.
Search, recipe and limits
New in 5.4.0. Meilisearch for Scout, the deploy-config options (releases kept, migrate,
optimize, extra artisan, snapshot) and per-app memory, FPM, Octane and worker limits — out of bounds
blocks the plan instead of being clamped. The 5.5 disk limit stays server-side, out of the
repository's reach.
HTTPS, required env and crons
New in 5.4.0. Force HTTPS once the certificate exists. Block a release while a named
.env variable is missing. Scheduled commands through allowlisted runners, in tagged
crontab lines that never touch the rest.
A Node app's build
New in 5.4.0. Framework, mode, Node major, build and start commands, output and health path — read by the deploy from that release, so the commit that changes the start command is built and started with it.
node: 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 # 5.4.0+: canonical www redirect and basic auth (names or hashes, never passwords) www: to-root basic_auth: users: - admin # 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 # Laravel Reverb (5.1.2+). Cipi gives it a localhost port, a Supervisor program # and an nginx proxy for /app/{key} and /apps/{id}/… on this app's own domain, and # generates REVERB_APP_ID/KEY/SECRET (plus the VITE_ copies) in the .env the first # time it is enabled. An app with its own /app or /apps route cannot use it behind # the same domain — those paths belong to the Pusher protocol. reverb: false 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. # To remove an existing healthcheck, write only health: { enabled: false } (honoured since 5.3.0). 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) # Post-deploy steps (5.2.3+). Allowlisted runners only — artisan, npm/npx/yarn/pnpm, # composer, or one relative php/node script under the release. No free shell. # This is the one section that runs WITHOUT `cipi yml auto` being on. deploy: post: - artisan cache:clear - artisan scout:import --force - npm run build - composer dump-autoload -o # post_on_failure: abort # default warn: log + email, release stays live # 5.4.0+: the cipi app deploy-config options, e.g. # keep_releases: 5 # snapshot: true # 5.4.0+: Meilisearch for Scout (the engine is installed by root first) search: false # 5.4.0+: names (never values) the .env must carry, and allowlisted crons env: required: [ STRIPE_KEY ] crons: - every: 30m run: artisan queue:prune-batches # 5.4.0+: path redirects and reverse proxy prefixes (each list replaces the current one) redirects: - from: /blog/ to: "https://blog.myapp.com/" proxies: - prefix: /api/ upstream: "http://127.0.0.1:3000" strip_prefix: true # 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
deploy.post — the one exception (5.2.3)
Post-deploy steps are the single section that does not wait for
cipi yml auto. Commit a deploy.post list and it runs from the live release after
every successful deploy — cipi deploy and the Git webhook alike. It can afford to, because it
accepts no shell: only artisan, npm/npx/yarn/pnpm,
composer, and one relative php or node script under the release.
Unknown runners, bash, pipes, ;, .. in paths and
artisan tinker are all refused.
The order is: Deployer finishes → optional yml apply → deploy.post →
post-deploy healthcheck → success notification. A failing step logs and emails
yml_post_fail while the release stays live; set
deploy.post_on_failure: abort to make cipi deploy fail with it instead — useful
in CI when npm run build must pass.
$ cipi yml post-deploy myapp # run the steps now, against current/ $ cipi yml plan myapp # lists them under "After deploy"
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.
deploy.post(5.2.3) andcrons(5.4.0) are the sections that run something, and they take allowlisted runners only — never a shell line. - Basic auth accepts user names or bcrypt / SHA-512 hashes — never a password, and never apr1/MD5.
- Proxies declared in the file cannot target Cipi's own local ports, link-local addresses or cloud
metadata; resolved hostnames are checked as well as literal IPs.
ssl.force_httpscan only be turned on, and out-of-bounds limits block the plan instead of being clamped. - 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, backup profiles, basic auth users, redirects, proxies, crons and a
Node app's build. 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 — with the single
exception of deploy.post (5.2.3), which runs a fixed
allowlist of runners and no shell.
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. deploy.post runs on both
too, and does not need the opt-in.