All pages
Compose files
A single app that the platform already recognises needs no file at all. A compose file is for the second thing: a database beside the web service, a worker, a shared app name, a volume.
A file that covers the common case
compose.pilots.yaml
name: shop
services:
web:
build: ./web
environment:
DATABASE_URL: secret://database_url
depends_on: [postgres]
postgres:
image: postgres:17
volumes:
- pgdata:/var/lib/postgresql/data
x-pilots:
size_gib: 20
volumes:
pgdata: {}
Deploy it with pilot deploy, which picks up
compose.pilots.yaml first and compose.yaml second. That order is the
point: a repository that also runs under docker compose on a laptop keeps its
compose.yaml with host ports and a local database, and says how it deploys HERE
in the pilots file.
- The file's text and the directory's
.envgo to the fleet. The CLI interpolates nothing, so there is one parser and it is the one beside the daemon. - The plan comes back as an ordered list of steps in dependency order: build, volume, service, deploy, per service.
- Services in the same app find each other at
<name>.internal, which is what a connection string in the file should point at.
The x-pilots keys
Everything pilots needs that compose has no word for goes under
x-pilots on a service, or at the top level for the app name.
| Key | What it does |
|---|---|
domain
| The label the URL is minted from. Defaults to the service name. |
private: true
|
No URL at all. Peers still reach it at <name>.internal.
|
custom_domain
| A hostname you own (URLs and domains). |
auto_stop
|
suspend, the default, sleeps an idle replica. off never
does.
|
auto_start
| Wake on a request. True by default, and false is a dead URL. |
min_machines_running
| Replicas kept resident. Zero by default, which is scale to zero. |
soft_limit
| Concurrent requests per replica before another starts. 20 by default. |
idle_timeout
|
Quiet before a machine sleeps, as 30m or as seconds, up to an hour.
This is the sandbox timer, carried by a promoted machine. A service replica's
scale-down window belongs to the autoscaler and is not a knob.
|
schedules
| Cron jobs (scheduled jobs). An empty list clears the previous release's. |
size_gib
|
The size in gibibytes of every named volume this service mounts. Ten by
default. It sits on the SERVICE, because the top-level volumes entry is a
name and nothing else.
|
pre_deploy
| A command run on a throwaway machine before any replica is replaced. This is where a migration goes. |
app
|
Top level only. The app name, which a top-level name: also sets.
|
services:
web:
build: ./web
x-pilots:
min_machines_running: 1
soft_limit: 40
pre_deploy: npm run migrate
worker:
build: ./worker
x-pilots:
private: true
auto_stop: "off"
The rules
| Rule | Why |
|---|---|
Variables come from the .env FILE, never from your shell
| A deploy has to be reproducible from the checkout. |
| One build context per service | Each service is its own image. |
| A volume-backed service runs one replica | A volume is mounted by one machine at a time, and two writers is a corrupted filesystem. |
A secret:// reference, never a value
| The file is committed and the value is not (secrets). |
A git push deploys the service the repository is linked to, by name
| A compose file may describe several, and the push builds the one the link names. |
When it is refused
| Answer | Do |
|---|---|
plan_unsupported
| The file asks for something pilots does not do. Every unsupported key is listed at once, so one pass fixes them all. |
compose_invalid
| The file does not parse, or a variable is unset. The message names the file and the problem. |
bad_request about the app name
|
Nothing names the app. Add a top-level name:, set the project
environment variable, or pass --app.
|