Dockflow

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

compose.yml
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-first

Files, profiles and variables

  • Which files. The compose list of dockflow.yml, or of the environment, which replaces it. Without one, the first that exists of compose.yaml, compose.yml, docker-compose.yaml and docker-compose.yml. An app with no Compose file is valid.
  • Several files are merged in order, as docker compose -f a.yml -f b.yml does. include and extends work as in Compose.
  • Profiles. A service with profiles is deployed only when the environment lists one of its profiles under profiles. Services without profiles are always deployed.
  • Project name. Always <app>-<env>. A top-level name in the Compose file has no effect.
  • Interpolation. ${VAR}, ${VAR:-default}, ${VAR:?message} and $$, with Dockflow's variables: built-ins, secrets, then vars. A variable with no value and no default is an error. The Compose .env file is not read.
  • environment: [KEY], a key without a value, takes the value of that variable.
  • Relative paths, such as build.context and env_file, are relative to the folder of the first Compose file, as with docker compose -f deploy/compose.yml: a Compose file in deploy/ builds the project with context: ... env_file is read on your machine.
  • Names on the server. Containers, networks and volumes are prefixed with the stack: the volume pgdata of shop in production is shop-production_pgdata. Networks and volumes with external: true keep their own name and must already exist on the server.
  • x-* keys other than x-dockflow, at the top level or on a service, are ignored, so YAML anchors under x-common work.

Service keys

KeyHandling
imageRequired unless build is set. Pulled according to pull_policy, and pinned to its digest in the release.
buildBuilt by Dockflow. See Build. When build is set, image is not used: the image is named by Dockflow. See Image names.
pull_policymissing (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, scaleThe number of containers of the service.
deploy.resourceslimits.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_policyUsed when restart is not set: condition: none becomes no, on-failure becomes on-failure (with max_attempts), any becomes always.
deploy.update_config.orderstart-first or stop-first: the rollout strategy.
deploy.modereplicated. global runs one container, with a warning: one per server, on one server.
deploy.placement, deploy.endpoint_mode, deploy.rollback_configIgnored with a warning: there is one server, and Dockflow rolls back the whole app.
restartHonoured. A service without restart or deploy.restart_policy gets unless-stopped. A job always gets no.
depends_onHonoured 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.
healthcheckEvery key (test, interval, timeout, retries, start_period, start_interval, disable). It gates every rollout strategy.
container_nameHonoured. Forces stop-first and a single container.
portsHonoured, 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.
exposeHonoured.
networksBridge 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_modehost, 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.
volumesNamed 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_fromRefused, for the same reason as network_mode: service:: mount the same named volume in both services.
linksA 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.
gpusPassed to the container as device requests, with the gpu capability.
environment, env_fileHonoured. env_file is read on your machine.
profilesSee 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_optPassed to the container, as Compose does.
developIgnored: it describes a development loop on your machine.
secretsFiles 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.
configsFiles 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 keyRefused 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 keyHandling
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, entitlementsPassed to bake. dockerfile is relative to context, as in Compose.
privilegedAdds the security.insecure entitlement.
platforms, tagsIgnored with a warning: images are built for the server's platform and tagged by Dockflow.
secretsMounted in the build with RUN --mount=type=secret; the value reaches the builder through its environment. See Secrets in builds.
isolation, ulimits, provenance, sbomRefused.

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

ServiceImage the containers run
With image onlyThat image, pulled on the server according to pull_policy, pinned to its digest in the release.
With build, no registry blockPushed 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.urlPushed as <registry.url>/<app>/<service>:<version>, plus every registry.additional_tags, and run by digest.
With build and build.builder: remote, no registry blockBuilt 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.

KeyTypeDefaultDescription
routeslistnoneDomains and paths the switch sends to this service. See routes.
rolloutmapsee rolloutHow the service is replaced.
healthmapsee healthWhen a new container counts as healthy.
statefulbooleanfalseA service that holds data, like a database: one container, replaced with stop-first.
runstringnonebefore or after: the service is a job, run to completion on each deploy.
backupmapnoneHow 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.

KeyTypeDefaultDescription
domainslist of stringsrequiredThe host names.
portintegerrequiredThe container port.
pathstring/A path prefix, starting with /. The route matches the path and everything under it; the longest path wins.
strip_prefixbooleanfalseRemove path from the request before it reaches the service.
tlsbooleantrueWith 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.
headersmapnoneResponse headers to set. A name starting with - removes that header, with an empty value: -Server: "".
encodelistnoneCompressions offered to clients that accept them: zstd, gzip, in order of preference.
max_bodysizenoneThe largest request body: 25MB, 512k, or a number of bytes, in powers of 1024. A larger one gets a 413.
timeoutsmapCaddy'sread, write and dial: durations for the switch's connection to the containers.
authmapnoneHTTP basic authentication: user, without a colon, and password, from a secret: password: ${PANEL_PASSWORD}. Both are required. The switch keeps only a bcrypt hash.
caddystringnoneRaw 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_pagesmapnoneStatuses 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}:4327

The 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

KeyTypeDefaultDescription
strategystringsee belowswitch, start-first or stop-first. Takes precedence over deploy.update_config.order.
drainduration30sWith 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:

ServiceStrategy
stateful, with container_name, publishing a fixed host port, or on network_mode: hoststop-first
Otherwise, with routesswitch
Otherwisestart-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

KeyTypeDefaultDescription
graceduration10sFor a container without a healthcheck and without http: how long it must keep running to count as healthy.
httpmapnoneAn HTTP check of each new container, made from the server before the switch.
restart_afterduration2mHow long a container of the current release may stay unhealthy before the watchdog restarts it. 0 never restarts.

http:

KeyTypeDefaultDescription
pathstring/The path requested, starting with /.
portintegerthe first route's portThe container port. Required on a service without routes.
statusnumber or string200-399The accepted status: one status (204) or a range ("200-399").
timeoutduration5sTime allowed for one request.
retriesinteger10Attempts before the check fails. At least 1.
intervalduration3sTime 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.

ValueWhen it runs
beforeAfter networks and volumes are created, before any service is updated, like a database migration.
afterAfter 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

CombinationWhy
routes or health with runA job serves no traffic.
stateful with runA job is not a long-running service.
run with deploy.replicas above 1A job runs once.
stateful or container_name with deploy.replicas above 1The service runs once.
rollout.strategy: switch without routesNothing to switch.
start-first or switch on a service that must use stop-firstSee rollout.
routes with network_modeA routed service must be on bridge networks.
backup without statefulOnly stateful services hold data to back up.
run: after on a service another one waits for with service_completed_successfullyIt 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:

compose.yml
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:

dockflow.yml
version: 1
app: shop
environments:
  production: {}
  staging:
    compose: [compose.yml, compose.staging.yml]
    profiles: [debug]
compose.staging.yml
services:
  debug:
    image: busybox
    command: ["sleep", "infinity"]
    profiles: [debug]
Edit on GitHub

On this page