Since Cipi 5.1

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.

20+ keysAliases, PHP, databases, workers, health, backups, routes, basic auth, search, limits, crons, Node.
plan → applyNothing changes until you look at the diff and say yes.
Fail-closedScoped to one app, allowlisted runners only, no shell anywhere in the schema.

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

bash
$ 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

app.aliases

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.

Aliases in the docs →
app.php · app.ini

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.

php.ini in the docs →
databases

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.

Databases in the docs →
workers

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.

Workers in the docs →
workers.reverb

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.

Reverb in the docs →
schedule

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.

Scheduler in the docs →
health

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.

Health checks in the docs →
backup.profiles

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.post

Post-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.

deploy.post in the docs →
app.www · app.basic_auth

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.

www and basic auth →
redirect · redirects · proxies

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.

Routes in the docs →
search · deploy · app.limits

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.

More keys in the docs →
ssl · env.required · crons

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.

More keys in the docs →
node

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 →
reference

Every 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.

bash
$ cipi yml generate myapp > cipi.yml   # then commit it
$ cipi yml plan myapp                  # reports nothing to do
yaml
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.

bash
$ 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.

bash
$ 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:

Once 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.

Full cipi.yml reference Practical guide Install Cipi