Skip to content
pilots
Dashboard
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.

  1. The file's text and the directory's .env go to the fleet. The CLI interpolates nothing, so there is one parser and it is the one beside the daemon.
  2. The plan comes back as an ordered list of steps in dependency order: build, volume, service, deploy, per service.
  3. 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.