Dockflow

dockflow.yml

Every key of dockflow.yml, with its type, default and meaning, the naming rules and the order in which variables are looked up.

dockflow.yml sits at the root of your project and describes the app and its environments. Your services stay in standard Compose files. dockflow.yml holds what Compose has no place for: the environments, the registry, the hooks, the checks and the uploads.

This page lists every key, section by section, starting with the top level. A complete example is at the end.

The format is strict: an unknown key is an error (exit code 3, see exit codes). Later 1.x versions may add keys, but never remove or rename one. A file that uses a newer key says so with requires. dockflow validate <env> checks the file, the Compose files and the variables without contacting a server.

Conventions

  • Durations are written like 90s, 5m, 1h30m or 500ms (units ms, s, m, h).
  • Paths are relative to the project directory, the one that holds dockflow.yml, and must stay inside it unless stated otherwise.
  • String values are interpolated with Compose syntax (${VAR}, ${VAR:-default}, ${VAR:?message}, $$ for a literal $), except version, app, mapping keys and the values under environments.<env>.vars. See variables.

Top level

KeyTypeDefaultDescription
versionintegerrequiredThe format version. Must be 1. A higher version stops with config.newer (exit code 3) and asks for an upgrade.
requiresstringnoneThe oldest Dockflow that reads this file, such as "1.2" or "1.2.0". An older Dockflow stops with config.newer and asks for an upgrade, instead of refusing a key it does not know. Set it when you use a key added after 1.0. A development build (dev) does not check it.
appstringrequiredThe app name. See naming rules. Never interpolated.
composelist of pathsthe first that exists of compose.yaml, compose.yml, docker-compose.yaml, docker-compose.ymlThe Compose files, merged in order. A listed file that does not exist is an error. An app with no Compose file at all is valid.
templateslistnoneFiles rendered with Jinja before the build and the uploads. See templates.
registrymapnone: the server's own registryAn external registry for built images. See registry.
buildmapbuilder: local, engine: dockerHow images are built. See build.
releasesmapkeep: 10Release retention. See releases.
on_failurestringrollbackWhat a failed HTTP check does. See on_failure.
healthmapnonePublic checks, run after the switch. See health.
uploadslistnoneFiles and directories copied to the server. See uploads.
hooksmapnoneCommands run around the build and the deploy. See hooks.
notifylistnoneWebhooks sent when a run finishes. See notify.
firewallmapnoneThe published ports open to the internet. See firewall.
pluginslistnonePlugins expanded into uploads, templates and hooks. See plugins.
routeslistnoneRoutes that target no service of the app: redirects, proxies, files. See routes.
backupsmapnone: a repository on the serverWhere backups go. See backups.
environmentsmaprequired, at least oneThe environments, by name. See environments.

Keys an environment can override

These top-level keys can also be set inside an environment, where they replace the top-level value for that environment (blocks are not merged key by key):

templates, registry, build, releases, on_failure, health, uploads, hooks, notify, firewall, plugins, routes, backups.

version, requires, app, compose and environments cannot be set inside an environment. An environment has its own compose key, which replaces the top-level list.

templates

Each entry is either a path, rendered in place, or a src/dest pair. Templates are rendered into a temporary copy of the build contexts and the upload sources: your source tree is never written to. An undefined variable is an error. The template syntax is in the templates guide.

KeyTypeDefaultDescription
(a plain string)pathA file rendered in place: src and dest are the same path.
srcpathrequiredThe template file, inside the project.
destpathsrcWhere the rendered file goes, inside the project (in the temporary copy).

registry

Without this block, built images are pushed to the registry Dockflow runs on the server, through the SSH connection.

KeyTypeDefaultDescription
urlstringrequired when registry is setThe registry and namespace, like ghcr.io/acme. A leading https:// or http:// and a trailing / are removed. Images are named <url>/<app>/<service>.
usernamestringnoneThe user to push and pull with. Goes with password.
passwordstringnoneThe password or token. Goes with username; usually a secret: ${GHCR_TOKEN}.
additional_tagslist of stringsnoneTags pushed in addition to the release's version tag, like "${DOCKFLOW_ENV}-latest".

build

KeyTypeDefaultDescription
builderstringlocallocal builds on the machine that runs dockflow; remote builds with the server's Docker.
enginestringdockerdocker (buildx bake) or podman. Podman builds locally only, so engine: podman with builder: remote is refused.

See the building images guide.

releases

KeyTypeDefaultDescription
keepinteger10Releases kept per environment, with their images. At least 2.

on_failure

ValueEffect
rollbackThe default. A failed HTTP check (x-dockflow.health.http or health.endpoints) rolls back to the previous release.
failThe failed release stays in place and the environment is marked degraded (exit code 9).
ignoreA failed HTTP check is reported as a warning and the release becomes active.

A failure of the rollout itself (a container that exits, stays unhealthy or times out) always rolls back, whatever the value. See releases and rollbacks.

health

Public checks, run from the server after traffic has moved to the new release. They add to the per-service checks of x-dockflow.health.

KeyTypeDefaultDescription
startup_delayduration0Time waited after the switch, before the first check.
endpointslistnoneThe URLs to check, in order.

Each entry of endpoints:

KeyTypeDefaultDescription
namestringthe URL's host and pathThe name shown in events.
urlstringrequiredAn http or https URL.
methodstringGETThe HTTP method.
expected_statusinteger200The status the URL must answer.
timeoutduration30sTime allowed for one request.
retriesinteger3Attempts before the check fails. At least 1.
retry_delayduration5sTime between two attempts.
validate_certsbooleantrueWhether TLS certificates are verified.

uploads

Files and directories copied to the server on each deploy, before the containers change. Only changed files are sent. See the uploads guide.

KeyTypeDefaultDescription
srcpathrequiredA file or directory inside the project. Templates apply to it.
deststringrequiredvolume:<name>, a volume declared in the Compose files, or an absolute path on the server. Two uploads cannot share a destination.
mirrorbooleanfalseDelete files at the destination that are not in the source.
excludelist of stringsnonePatterns of paths to leave out, gitignore style.
modestringunchangedAn octal file mode like "644" or "0644". Quote it, so YAML keeps it a string.
ownerstringunchangedA numeric owner, uid or uid:gid.

hooks

Commands run at fixed points of a deploy. Hook commands are not interpolated. Instead, the built-in variables, the environment's vars and the secrets the hook lists arrive as environment variables. See the hooks guide.

KeyTypeDefaultDescription
before_buildlist of hooksnoneBefore images are built.
after_buildlist of hooksnoneAfter images are built and pushed.
before_deploylist of hooksnoneBefore the containers change.
after_deploylist of hooksnoneAfter the release is live.
after_failurelist of hooksnoneAfter a deploy failed.

Each hook sets exactly one of run, script, host and exec:

KeyTypeDefaultDescription
runstringA shell command, run with sh -c on the machine running dockflow, in the project directory.
scriptpathA script of the project, run on the machine running dockflow (directly when executable, with sh otherwise).
hoststringA shell command run on the server over SSH.
execlist of stringsA command run in every running container of service. Allowed from before_deploy on, not in before_build or after_build.
servicestringrequired with execThe service whose containers run exec. Only valid with exec.
namestringderived from the commandThe name shown in events.
secretslist of keysnoneSecrets passed to the hook as environment variables. A listed secret without a value is an error.
timeoutduration5mTime allowed before the hook is stopped and counts as failed.
fatalbooleantrue in before_* phases, false otherwiseWhether a failure stops the deploy. A fatal before_* hook stops it before anything changed (exit code 11); a fatal after_deploy hook rolls the release back (exit code 8). A non-fatal failure is a warning.

notify

Webhooks sent when a run finishes. See the notifications guide.

KeyTypeDefaultDescription
urlstringrequiredAn http or https URL that receives a POST.
onlist of stringsbothsuccess, failure, or both.
secretstringnoneWhen set, the body is signed with HMAC-SHA256 and the signature sent in X-Dockflow-Signature: sha256=<hex>.
headersmap of stringsnoneExtra request headers.
timeoutduration10sTime allowed for the request.

firewall

The ports your services publish that the port filter lets in from outside. A port published on every interface (5432:5432) and not listed here is dropped, and the plan warns about it.

firewall:
  public_ports:
    - 25565                                            # tcp and udp, from anywhere
    - { port: 4326, protocol: tcp, from: [203.0.113.0/24] }

An entry of public_ports is a port number, or a map:

KeyTypeDefaultDescription
portintegerrequiredThe published (host) port, 1 to 65535.
protocolstringbothtcp or udp.
fromlist of stringsanywhereAddresses or CIDRs, IPv4 or IPv6, the port is open to.

There is one filter per server. A port that one app lists is open, whichever container publishes it. The filter follows each app environment's current release, so a rollback restores the ports of the release it returns to.

plugins

Plugins to expand into this environment's uploads, templates and hooks. See the plugins guide.

plugins:
  - use: nginx
    with: { domain: "${DOMAIN}", cloudflare: true }
  - use: ./deploy/plugins/rclone
    id: archives
    with: { remote: "s3:archives" }
KeyTypeDefaultDescription
usestringrequiredA built-in plugin, nginx or systemd, or the path of a directory of the project that holds a plugin.yml. A value with a / (./deploy/plugins/rclone) is a path; it must stay inside the project.
idstringthe plugin's nameNames the use: in hook names, the plan, and the plugin's own paths. Matches ^[a-z][a-z0-9_-]{0,31}$; two uses of one plugin need different ids.
withmapnoneThe plugin's inputs. An unknown input, or a required one missing, is an error. Values are interpolated like the rest of the file.

routes

Routes the switch serves that don't target a service of the app. Each has exactly one of redirect, proxy and files.

routes:
  - domains: [example.com]
    path: /docs
    redirect: "https://docs.example.com{uri}"
  - domains: [api.example.com]
    proxy: http://billing-api:8080      # another app's service, on a shared network
    network: shared
  - name: maintenance
    domains: [example.com]
    path: /maintenance
    files: { host_path: /srv/maintenance }
  - domains: [files.example.com]
    files: { volume: archives, browse: true }
    auth: { user: team, password: "${ARCHIVES_PASSWORD}" }
KeyTypeDefaultDescription
namestringnoneNames the route, for the error_pages of other routes. Matches ^[a-z][a-z0-9-]{0,31}$; two routes cannot share a name.
domains, path, tls, strip_prefixAs on a service's route (Compose reference). domains is required.
redirectstringAn http or https URL, or a path starting with /, to redirect to. Caddy's placeholders work: {uri} is the request's path and query.
statusinteger308Only with redirect: 301, 302, 303, 307 or 308.
proxystringAn http or https URL without a path, http://host:port, to send requests to. The Host header is the URL's, as nginx's proxy_pass does.
networkstringnoneWith proxy: a shared network the switch joins to reach the URL's host.
filesmapServe a directory: host_path, an absolute path on the server, or volume, a volume of the Compose file; browse: true lists directories. The route's path is stripped: /builds-raw/a.zip is a.zip of the directory.

A route also takes every option of a service's route: headers, encode, max_body, timeouts, auth, caddy and error_pages.

A files route runs a small file server, files-<name> (or files-<n>), in the switch's image. That file server mounts the directory read-only; the switch itself mounts nothing.

environments

A map from environment name to its settings. At least one environment is required. An environment can be empty (production: {}) when its server comes from the <ENV>_CONNECTION secret.

KeyTypeDefaultDescription
servermapnoneThe server, when no connection string is used. See server.
composelist of pathsthe top-level composeReplaces the top-level list for this environment.
profileslist of stringsnoneCompose profiles to enable. Services with a profile not listed here are left out.
varsmap of stringsnonePlain values for interpolation and templates. Keys follow the key rule; values are taken literally, never interpolated.
secrets_filepathsecrets/<env>.sops.yaml when it existsThe environment's SOPS file, encrypted with age: a flat map of secrets, after CI variables and .env.dockflow. See Secrets in a SOPS file.
deploy.timeoutdurationcomputedReplaces the computed time every service is given to become healthy, and the time a job is given to finish (30m by default).
ci.tag_suffixstring"" for production, the environment's name for the othersThe tag suffix that selects this environment in CI: "" means bare versions (1.2.0), staging means 1.2.0-staging.

It can also hold any of the keys an environment can override.

server

How Dockflow finds the server of an environment, first match wins:

  1. the --connection option;
  2. the <ENV>_CONNECTION secret (PRODUCTION_CONNECTION), printed by dockflow server connection;
  3. the server block, with the private key from DOCKFLOW_SSH_KEY_<ENV>, DOCKFLOW_SSH_KEY (the key itself, or a path to it), ssh-agent, or the IdentityFile your ~/.ssh/config gives the host.

Dockflow reads HostName, Port, User and IdentityFile from ~/.ssh/config as OpenSSH does: Host patterns (with *, ? and !), Include, and the first value of each keyword. Match blocks are skipped. What server: sets wins over the file.

KeyTypeDefaultDescription
hoststringrequiredThe server's address, or a Host alias of your ~/.ssh/config.
portintegerPort of ~/.ssh/config, else 22The SSH port.
userstringUser of ~/.ssh/config, else dockflowThe SSH user. It must be able to use Docker.
host_keystringnoneThe server's host key fingerprint, SHA256:…. Without it, the key must be in ~/.ssh/known_hosts. A key that is neither pinned nor known is refused.
namestringmainThe server's name, available as DOCKFLOW_SERVER. Matches ^[a-z][a-z0-9_-]{0,31}$.

backups

Where the environment's backups go. Without this block, backups go to a restic repository on the server, in the dockflow-backups volume, with a password Dockflow generates.

KeyTypeDefaultDescription
repositorystringrequiredAn off-site restic repository: s3:, b2:, azure:, gs:, sftp: or rest:. A path is refused: leave backups out for the repository on the server.
passwordstringgeneratedThe repository's password, from a secret: ${RESTIC_PASSWORD}. Without it, Dockflow generates one and stores it in /var/lib/dockflow/backups/ on the server.
envmapnoneVariables passed to restic: the storage's credentials, such as AWS_ACCESS_KEY_ID.
dockflow.yml
backups:
  repository: s3:s3.eu-west-1.amazonaws.com/acme-backups/${DOCKFLOW_APP}
  password: ${RESTIC_PASSWORD}
  env:
    AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
    AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}

Naming rules

NameRuleNotes
app^[a-z][a-z0-9-]{0,22}[a-z0-9]$, 2 to 24 charactersPart of every container, network and volume name.
environment^[a-z][a-z0-9]{0,11}$, at most 12 charactersNo hyphen and no underscore: it is also the prefix of variable names (PRODUCTION_…).
stack<app>-<env>The Compose project name of an environment. Not configurable.
service^[a-z0-9][a-z0-9_.-]{0,62}$, at most 63 charactersChecked on the services of the Compose files.
keys of vars, hook secrets^[A-Z][A-Z0-9_]*$Upper case, valid in every CI provider.

On the command line, an environment can be shortened to a unique prefix: dockflow deploy prod deploys production. When dockflow.yml has a single environment, it can be left out.

Variables

Values are written in the syntax native to each file:

WhereSyntax
Compose files, string values of dockflow.ymlCompose interpolation: ${VAR}, ${VAR:-default}, ${VAR:?message}, $$ for a literal $
Files listed under templatesJinja: {{ VAR }}, {{ VAR | default('x') }}, {% if %}, {% for %}, {% raw %}
Hook commandsNot interpolated: values arrive as environment variables

A variable with no value and no default is an error, in both syntaxes.

Lookup order

For an environment, here production, a variable is looked up in this order, and the first match wins:

  1. Built-in variables:

    VariableValue
    DOCKFLOW_APPThe app name
    DOCKFLOW_ENVThe environment name
    DOCKFLOW_STACK<app>-<env>
    DOCKFLOW_VERSIONThe release's version label: --version, else the version of the CI tag, else the short commit (with -dirty when the tree has changes), else the UTC time of the deploy
    DOCKFLOW_GIT_SHAThe full commit, empty outside git
    DOCKFLOW_GIT_BRANCHThe branch, empty on a detached checkout unless the CI provider gives it
    DOCKFLOW_SERVERThe server's name (main by default)
  2. Secrets. The key X is looked up as PRODUCTION_X, then X, in each source in turn:

    1. the process environment, of which only PRODUCTION_* variables are read, and the JSON object in DOCKFLOW_SECRETS;
    2. the file .env.dockflow at the project root (KEY=value lines, values taken literally);
    3. the environment's SOPS file, when there is one.
  3. The environment's vars.

Names with a double underscore right after the environment's prefix, such as PRODUCTION__DB__PASSWORD, are reserved for per-server values, for when an environment has several servers. Do not define such variables.

Other process variables are never read: ${HOME} or ${CI} are undefined unless you set them in vars or as secrets. A Compose .env file is not read either.

Only what the environment being deployed uses is interpolated: the blocks of other environments, and the top-level keys this environment overrides, are not. A variable used only in environments.staging need not resolve when you deploy production.

Interpolating DOCKFLOW_VERSION or DOCKFLOW_GIT_SHA into a service changes its spec on every commit, so that service is restarted by every deploy.

A complete example

This file and its Compose files pass dockflow validate for both environments, with the secrets GHCR_TOKEN, JWT_PRIVATE_KEY and WEBHOOK_SECRET set (for example PRODUCTION_GHCR_TOKEN=…).

dockflow.yml
version: 1
app: shop

# Compose files, merged in order.
compose: [compose.yml]

# Rendered with Jinja into a temporary copy, before the build and the uploads.
templates:
  - src: config/app.conf.tpl
    dest: config/app.conf
  - nginx/default.conf              # rendered in place, in the copy

# Without this block, images go to the server's own registry.
registry:
  url: ghcr.io/acme                 # images are ghcr.io/acme/shop/<service>
  username: acme-bot
  password: ${GHCR_TOKEN}           # a secret: PRODUCTION_GHCR_TOKEN in CI
  additional_tags: ["${DOCKFLOW_ENV}-latest"]

build:
  builder: local                    # local or remote
  engine: docker                    # docker or podman

releases:
  keep: 10                          # at least 2

on_failure: rollback                # rollback, fail or ignore

# Public checks, after the switch.
health:
  startup_delay: 5s
  endpoints:
    - name: site
      url: https://${DOMAIN}/
      expected_status: 200
      timeout: 30s
      retries: 3
      retry_delay: 5s

uploads:
  - src: public
    dest: volume:static             # a volume of the Compose file
    mirror: true                    # delete what is not in public/
    exclude: [.git, "*.map"]
  - src: deploy/backup.timer
    dest: /etc/systemd/system/shop-backup.timer
    mode: "644"

hooks:
  before_build:
    - run: ./scripts/generate-keys.sh
      secrets: [JWT_PRIVATE_KEY]    # arrives as an environment variable
  before_deploy:
    - name: clear cache
      service: web
      exec: ["sh", "-c", "rm -rf /var/cache/nginx/*"]
      timeout: 2m
  after_deploy:
    - host: sudo systemctl daemon-reload
      fatal: false

notify:
  - url: https://hooks.example.com/dockflow
    on: [failure]
    secret: ${WEBHOOK_SECRET}
    headers: { X-Team: shop }
    timeout: 10s

environments:
  production:                       # server from the PRODUCTION_CONNECTION secret
    vars:
      DOMAIN: shop.example.com
    deploy:
      timeout: 15m
  staging:
    server:                         # instead of a STAGING_CONNECTION secret
      host: 203.0.113.20
      user: dockflow
      host_key: "SHA256:2kVJbNxZ4m8l9Pq0vXyQ1sXJmVf5tZ8b3c7d6e5f4g0"
    compose: [compose.yml, compose.staging.yml]
    profiles: [debug]
    vars:
      DOMAIN: staging.shop.example.com
    on_failure: fail                # replaces the top-level value
    releases:
      keep: 3
    ci:
      tag_suffix: staging           # the tag 1.2.0-staging deploys here
compose.yml
services:
  web:
    build: .
    volumes: [static:/usr/share/nginx/html]
    x-dockflow:
      routes:
        - domains: ["${DOMAIN}"]
          port: 80
      health:
        http: { path: / }

volumes:
  static: {}
compose.staging.yml
services:
  debug:
    image: busybox
    command: ["sleep", "infinity"]
    profiles: [debug]
Edit on GitHub

On this page