Compose
How Dockflow reads your Compose files, key by key, and every option of the x-dockflow extension.
Your services are described in standard Compose files, read with the same loader as Docker Compose. Dockflow's own per-service settings go under the x-dockflow extension.
This page lists how each Compose key is handled, then every option of x-dockflow. Nothing is dropped silently:
- a key this version cannot apply is refused with exit code 3 (see exit codes);
- a key that has no meaning on one server is ignored with a warning.
Example
services:
web:
build: .
environment:
DATABASE_URL: postgres://shop:${DB_PASSWORD}@postgres:5432/shop
depends_on:
migrate: { condition: service_completed_successfully }
postgres: { condition: service_healthy }
deploy:
replicas: 2
x-dockflow:
routes:
- domains: ["${DOMAIN}", "www.${DOMAIN}"]
port: 3000
- domains: ["${DOMAIN}"]
path: /api
port: 3000
strip_prefix: true
rollout:
strategy: switch
drain: 30s
health:
http:
path: /healthz
status: 200-399
worker:
build: .
command: ["node", "worker.js"]
x-dockflow:
health:
grace: 15s
migrate:
build: .
command: ["node", "migrate.js"]
environment:
DATABASE_URL: postgres://shop:${DB_PASSWORD}@postgres:5432/shop
depends_on:
postgres: { condition: service_healthy }
x-dockflow:
run: before
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: shop
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes: [pgdata:/var/lib/postgresql/data]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U shop"]
interval: 5s
x-dockflow:
stateful: true
volumes:
pgdata: {}With DOMAIN in the environment's vars and DB_PASSWORD as a secret, dockflow list services shows what this file becomes:
SERVICE IMAGE REPLICAS STRATEGY ROUTES
migrate shop/migrate 1
postgres postgres:16-alpine 1 stop-first
web shop/web 2 switch shop.example.com/ www.shop.example.com/ shop.example.com/api
worker shop/worker 1 start-firstFiles, profiles and variables
- Which files. The
composelist ofdockflow.yml, or of the environment, which replaces it. Without one, the first that exists ofcompose.yaml,compose.yml,docker-compose.yamlanddocker-compose.yml. An app with no Compose file is valid. - Several files are merged in order, as
docker compose -f a.yml -f b.ymldoes.includeandextendswork as in Compose. - Profiles. A service with
profilesis deployed only when the environment lists one of its profiles underprofiles. Services withoutprofilesare always deployed. - Project name. Always
<app>-<env>. A top-levelnamein the Compose file has no effect. - Interpolation.
${VAR},${VAR:-default},${VAR:?message}and$$, with Dockflow's variables: built-ins, secrets, thenvars. A variable with no value and no default is an error. The Compose.envfile is not read. environment: [KEY], a key without a value, takes the value of that variable.- Relative paths, such as
build.contextandenv_file, are relative to the folder of the first Compose file, as withdocker compose -f deploy/compose.yml: a Compose file indeploy/builds the project withcontext: ...env_fileis read on your machine. - Names on the server. Containers, networks and volumes are prefixed with the stack: the volume
pgdataofshopinproductionisshop-production_pgdata. Networks and volumes withexternal: truekeep their own name and must already exist on the server. x-*keys other thanx-dockflow, at the top level or on a service, are ignored, so YAML anchors underx-commonwork.
Service keys
| Key | Handling |
|---|---|
image | Required unless build is set. Pulled according to pull_policy, and pinned to its digest in the release. |
build | Built by Dockflow. See Build. When build is set, image is not used: the image is named by Dockflow. See Image names. |
pull_policy | missing (default), if_not_present and build pull an image that is not on the server; always pulls on every deploy; never requires the image on the server. daily, weekly and every_<duration> pull on every deploy, with a warning: a deploy is when an image may change. |
deploy.replicas, scale | The number of containers of the service. |
deploy.resources | limits.memory, limits.cpus, limits.pids and reservations.memory, applied to each container. They take precedence over mem_limit, cpus, pids_limit and mem_reservation. reservations.devices asks for devices, as gpus does. reservations.cpus is ignored with a warning: Docker reserves no CPU outside Swarm. |
deploy.restart_policy | Used when restart is not set: condition: none becomes no, on-failure becomes on-failure (with max_attempts), any becomes always. |
deploy.update_config.order | start-first or stop-first: the rollout strategy. |
deploy.mode | replicated. global runs one container, with a warning: one per server, on one server. |
deploy.placement, deploy.endpoint_mode, deploy.rollback_config | Ignored with a warning: there is one server, and Dockflow rolls back the whole app. |
restart | Honoured. A service without restart or deploy.restart_policy gets unless-stopped. A job always gets no. |
depends_on | Honoured with its conditions (service_started, service_healthy, service_completed_successfully): it orders the services, and a service that another one waits for with service_completed_successfully becomes a job. A cycle is an error. |
healthcheck | Every key (test, interval, timeout, retries, start_period, start_interval, disable). It gates every rollout strategy. |
container_name | Honoured. Forces stop-first and a single container. |
ports | Honoured, host IP included (127.0.0.1:5432:5432). Reachable from outside only when listed under firewall.public_ports. A service that publishes a fixed host port uses stop-first, since two containers cannot hold the same port. |
expose | Honoured. |
networks | Bridge networks, with aliases, ipv4_address and ipv6_address. The service name is always an alias, as in Compose. Top-level networks keep their driver, internal and labels; external: true uses an existing network of the server, such as a shared network. |
network_mode | host, none and bridge. A service on the host network uses stop-first and cannot be routed. service:… and container:… are refused: Dockflow replaces containers at each release, so a container that joined another's network would lose it at the other's next deploy. Put both on a network instead. |
volumes | Named volumes (with nocopy and subpath), anonymous volumes, bind mounts (with create_host_path and propagation) and tmpfs mounts (with size). Top-level volumes keep their driver, driver_opts and labels; a driver other than local is a volume plugin the server must have. The bind option selinux is refused, because the Engine API cannot relabel a mount: label the directory once with chcon on the server. |
volumes_from | Refused, for the same reason as network_mode: service:: mount the same named volume in both services. |
links | A link orders the services, as depends_on would; the linked service is reached by its name already. A link with an alias (db:database) is refused: give the alias under networks.<name>.aliases. |
gpus | Passed to the container as device requests, with the gpu capability. |
environment, env_file | Honoured. env_file is read on your machine. |
profiles | See Files, profiles and variables. |
command, entrypoint, user, working_dir, hostname, domainname, labels, label_file, stop_signal, stop_grace_period, privileged, read_only, init, tty, stdin_open, cap_add, cap_drop, security_opt, extra_hosts, dns, dns_search, dns_opt, tmpfs, sysctls, ulimits, devices, group_add, pid, ipc, shm_size, mem_limit, mem_reservation, cpus, pids_limit, logging, platform, cpu_shares, cpu_period, cpu_quota, cpuset, memswap_limit, mem_swappiness, oom_kill_disable, oom_score_adj, runtime, userns_mode, cgroup, cgroup_parent, uts, annotations, storage_opt | Passed to the container, as Compose does. |
develop | Ignored: it describes a development loop on your machine. |
secrets | Files on a tmpfs of the server, mounted read-only at /run/secrets/<name> or target, with uid, gid and mode. Sources: environment: and file:. See Secrets and configs. |
configs | Files mounted read-only at /<name> or target. Sources: file:, content: and environment:. External secrets and configs, and their driver, belong to Swarm and are refused. |
name (top level) | Ignored with a warning: the project is always <app>-<env>. |
| Any other key | Refused with "not supported yet": blkio_config, cpu_count, cpu_percent, cpu_rt_period, cpu_rt_runtime, device_cgroup_rules, mac_address, external_links, post_start, pre_stop, credential_spec, isolation, use_api_socket, models and provider. |
Bind mount sources are paths on the server, so write them absolute. A relative source such as ./data names a directory of the machine running dockflow and is refused: use a named volume, or send the files with uploads.
Build
Services with build are built with docker buildx bake, or with Podman when build.engine: podman is set. The build runs on your machine, or on the server with build.builder: remote. See build and the building images guide.
| Build key | Handling |
|---|---|
context, dockerfile, dockerfile_inline, args, labels, additional_contexts, target, cache_from, cache_to, no_cache, no_cache_filter, pull, network, extra_hosts, ssh, shm_size, entitlements | Passed to bake. dockerfile is relative to context, as in Compose. |
privileged | Adds the security.insecure entitlement. |
platforms, tags | Ignored with a warning: images are built for the server's platform and tagged by Dockflow. |
secrets | Mounted in the build with RUN --mount=type=secret; the value reaches the builder through its environment. See Secrets in builds. |
isolation, ulimits, provenance, sbom | Refused. |
Podman builds use context, dockerfile, dockerfile_inline, args, labels, additional_contexts, target, cache_from, cache_to, no_cache, pull, network, extra_hosts, ssh and shm_size.
Files listed under templates are rendered into a temporary copy of the build contexts before the build.
Image names
| Service | Image the containers run |
|---|---|
With image only | That image, pulled on the server according to pull_policy, pinned to its digest in the release. |
With build, no registry block | Pushed through the SSH connection to the server's own registry as 127.0.0.1:5959/<app>/<service>:<version>, and run by digest. |
With build and registry.url | Pushed as <registry.url>/<app>/<service>:<version>, plus every registry.additional_tags, and run by digest. |
With build and build.builder: remote, no registry block | Built into the server's Docker as localhost/<app>/<service>, and run by image ID. |
<version> is the release's version label (DOCKFLOW_VERSION), with characters other than letters, digits, _, . and - replaced by -. In the plan and in dockflow list services, a built service shows as <app>/<service> until it is built.
x-dockflow
Dockflow's settings for one service. Values are interpolated like the rest of the file. Keys are matched exactly, in lower case. An unknown key, or a known one written in another case (Stateful), is an error.
| Key | Type | Default | Description |
|---|---|---|---|
routes | list | none | Domains and paths the switch sends to this service. See routes. |
rollout | map | see rollout | How the service is replaced. |
health | map | see health | When a new container counts as healthy. |
stateful | boolean | false | A service that holds data, like a database: one container, replaced with stop-first. |
run | string | none | before or after: the service is a job, run to completion on each deploy. |
backup | map | none | How the service is backed up; stateful services only. |
routes
Each route sends HTTP traffic from the server's switch to the service's containers, over the app's network. See the switch and edges.
| Key | Type | Default | Description |
|---|---|---|---|
domains | list of strings | required | The host names. |
port | integer | required | The container port. |
path | string | / | A path prefix, starting with /. The route matches the path and everything under it; the longest path wins. |
strip_prefix | boolean | false | Remove path from the request before it reaches the service. |
tls | boolean | true | With the builtin edge, serve the route over HTTPS with a certificate the switch obtains automatically, and redirect HTTP to it. false serves it over plain HTTP only. With the external edge, every route is served over HTTP to your own proxy. |
headers | map | none | Response headers to set. A name starting with - removes that header, with an empty value: -Server: "". |
encode | list | none | Compressions offered to clients that accept them: zstd, gzip, in order of preference. |
max_body | size | none | The largest request body: 25MB, 512k, or a number of bytes, in powers of 1024. A larger one gets a 413. |
timeouts | map | Caddy's | read, write and dial: durations for the switch's connection to the containers. |
auth | map | none | HTTP basic authentication: user, without a colon, and password, from a secret: password: ${PANEL_PASSWORD}. Both are required. The switch keeps only a bcrypt hash. |
caddy | string | none | Raw Caddyfile directives, run before the request reaches the service. {upstream} is the name of the service's first container. They are checked by the switch when the plan is computed. They run with the switch's access: see Raw Caddy directives. |
error_pages | map | none | Statuses from 400 to 599, separated by commas ("502,503,504"), mapped to { route: <name> }, a named route of dockflow.yml whose root is shown, or { body: "<html>…" }, an inline page. They replace the service's answers with these statuses, and the switch's own when no container can be reached, as while a stop-first service restarts. |
# compose.yml (fragment)
services:
panel:
image: ghcr.io/acme/panel:2.0
x-dockflow:
routes:
- domains: [panel.example.com]
port: 8080
headers:
X-Frame-Options: DENY
Strict-Transport-Security: max-age=31536000
-Server: ""
encode: [zstd, gzip]
max_body: 25MB
timeouts: { read: 90s }
auth: { user: admin, password: "${PANEL_PASSWORD}" }
caddy: |
@ws header Connection *Upgrade*
reverse_proxy @ws {upstream}:4327The switch applies these options with every edge, behind nginx too. Changing them reloads the switch's configuration once; an ordinary deploy does not reload it.
A domain belongs to one app environment on a server, and a domain and path pair can be routed by one service only. When a second app claims a domain another app routes, at any path, or a service claims a pair already routed, the plan fails with plan.domain_conflict, before anything changes. Domains are compared lower-cased and without a trailing dot, and paths without a trailing slash: /api/ is /api. Routes need a switch on the server, installed by dockflow server init.
Routes that do not target a service (redirects, proxies and files) go in routes: of dockflow.yml.
rollout
| Key | Type | Default | Description |
|---|---|---|---|
strategy | string | see below | switch, start-first or stop-first. Takes precedence over deploy.update_config.order. |
drain | duration | 30s | With switch: how long the old containers keep serving the requests in flight after the traffic has moved. |
The default strategy, when neither strategy nor deploy.update_config.order is set:
| Service | Strategy |
|---|---|
stateful, with container_name, publishing a fixed host port, or on network_mode: host | stop-first |
Otherwise, with routes | switch |
| Otherwise | start-first |
A service that must use stop-first (the first row) refuses any other strategy. switch needs routes. See rollouts.
A routed service that also publishes a fixed host port (ports: ["8080:3000"]) uses stop-first, with a short downtime on each deploy. Remove the published port to keep the switch.
health
| Key | Type | Default | Description |
|---|---|---|---|
grace | duration | 10s | For a container without a healthcheck and without http: how long it must keep running to count as healthy. |
http | map | none | An HTTP check of each new container, made from the server before the switch. |
restart_after | duration | 2m | How long a container of the current release may stay unhealthy before the watchdog restarts it. 0 never restarts. |
http:
| Key | Type | Default | Description |
|---|---|---|---|
path | string | / | The path requested, starting with /. |
port | integer | the first route's port | The container port. Required on a service without routes. |
status | number or string | 200-399 | The accepted status: one status (204) or a range ("200-399"). |
timeout | duration | 5s | Time allowed for one request. |
retries | integer | 10 | Attempts before the check fails. At least 1. |
interval | duration | 3s | Time between two attempts. |
A failed http check follows on_failure. A routed service with neither a healthcheck nor http gets a warning: it counts as healthy after grace. See the health checks guide.
stateful
stateful: true marks a service that holds data. It runs as a single container and is replaced with stop-first, so two containers never write the same volume.
A deploy updates the stateful services before the others (but after what they depend on), so the data is there before what uses it. Only stateful services take backup, and destroy keeps their volumes unless --volumes is given.
run
A job is a service run to completion on each deploy, in one container, never restarted. It fails the deploy unless it exits with code 0.
| Value | When it runs |
|---|---|
before | After networks and volumes are created, before any service is updated, like a database migration. |
after | After every service is updated. |
A service that another one waits for with depends_on: { condition: service_completed_successfully } is a before job, even without run. Jobs run on a deploy that changes something, in depends_on order. They do not run on rollback, restart or scale. See the jobs guide.
backup
How a stateful service is backed up with restic. The keys are type (postgres, mysql, mongo, redis, volume or command), database, dump_options, restore_options, volumes, quiesce (stop, none or { exec: { pre, post } }), dump, restore, and the retention keep. Each option is described in the backups guide.
# compose.yml (fragment)
services:
db:
image: postgres:17
x-dockflow:
stateful: true
backup: { type: postgres, keep: { daily: 14, weekly: 8 } }Refused combinations
| Combination | Why |
|---|---|
routes or health with run | A job serves no traffic. |
stateful with run | A job is not a long-running service. |
run with deploy.replicas above 1 | A job runs once. |
stateful or container_name with deploy.replicas above 1 | The service runs once. |
rollout.strategy: switch without routes | Nothing to switch. |
start-first or switch on a service that must use stop-first | See rollout. |
routes with network_mode | A routed service must be on bridge networks. |
backup without stateful | Only stateful services hold data to back up. |
run: after on a service another one waits for with service_completed_successfully | It would run after the service that waits for it. |
More examples
A job after the deploy, an unrouted service with an HTTP check, and a service that publishes a fixed port:
services:
api:
image: busybox
command: ["httpd", "-f", "-p", "8080"]
pull_policy: always
stop_grace_period: 20s
x-dockflow:
rollout: { strategy: start-first }
health:
http: { port: 8080, path: /, status: "200", timeout: 2s, retries: 5, interval: 2s }
notify:
image: curlimages/curl:8.10.1
command: ["curl", "-fsS", "https://hooks.example.com/deployed"]
x-dockflow:
run: after
game:
image: itzg/minecraft-server
ports: ["25565:25565"]
x-dockflow:
rollout: { strategy: stop-first }Several files, with a profile enabled in one environment only:
version: 1
app: shop
environments:
production: {}
staging:
compose: [compose.yml, compose.staging.yml]
profiles: [debug]services:
debug:
image: busybox
command: ["sleep", "infinity"]
profiles: [debug]