Dockflow

Server layout

What Dockflow puts on a server - the /var/lib/dockflow state directory, its containers, images, labels, networks, volumes and ports - and how to remove all of it by hand.

Dockflow keeps no database. Besides the switch, the registry and the watchdog, it runs no service of its own. All of its state for a server lives on that server: in /var/lib/dockflow, and in labels on the Docker objects it creates.

This page lists everything Dockflow creates on a server, so you can inspect it, back it up or remove it. The state format is specified in the state specification.

The state directory

/var/lib/dockflow/
├── server.json                       the server's settings
├── server.lock                       held while the switch's configuration changes
├── user-data-host-key                empty; present when server cloud-init prepared the server: the filter keeps containers from the metadata service
├── switch/routes/<stack>.json        the routes the switch serves for each app environment
├── backups/
│   ├── <repository>.password         a backup repository's password, when Dockflow generated it
│   └── <repository>.pruned           when restic last reclaimed the repository's space
└── apps/<stack>/
    ├── lock                          held by the running agent
    ├── lock.json                     who holds the lock
    ├── manual-lock.json              present while a manual lock is held
    ├── env.json                      current and previous release
    ├── releases/<seq>.json.gz        one record per release
    ├── configs/<name>-<hash12>       Compose configs, by content
    ├── secrets/<name>-<hash12>       copies of the Compose secret files, for the watchdog
    ├── watchdog.json                 the watchdog's latest restarts
    ├── backup.json                   what a backup or a restore in progress changed
    └── uploads/
        ├── blobs/<sha256>            uploaded files, and the files uploads replaced
        └── manifests/<digest>.json   what each upload and its replaced files contain

/run/dockflow/secrets/<stack>/        on a tmpfs, 0700
└── <name>-<hash12>                   Compose secrets, by content and permissions

/etc/dockflow/
├── filter.rules                      the port filter's IPv4 rules, for iptables-restore
└── filter6.rules                     its IPv6 rules
/etc/systemd/system/dockflow-filter.service   loads them at boot, before Docker
/etc/systemd/system/dockflow-cloudflare.timer  downloads Cloudflare's ranges every week (cloudflare edge)
/etc/systemd/system/dockflow-cloudflare.service  the unit the timer starts
/usr/local/libexec/dockflow-cloudflare-refresh the script it runs: an agent run

In these paths:

  • <stack> is <app>-<env>, for example shop-production;
  • <seq> is the release number, written with 6 digits in file names (000042.json.gz);
  • <repository> is local for the dockflow-backups volume, or the first 16 hex digits of the sha256 of the repository's URL.

About the files under /var/lib/dockflow:

  • Owner. Only agent containers write here, as root. The directory is 0700 and its files 0600: only root on the server can read them.
  • Atomic writes. Every file is written to <name>.tmp-<run-id>, synced, then renamed, so a crash never leaves a half-written file.
  • Format. Every JSON document carries "schema": 1. The format changes only in a major release of Dockflow.
  • Clocks. Timestamps are informational, in UTC, from the server's clock. No decision depends on them.

Files server init writes outside the state

Preparing a server (server init <admin>@<host>, sudo dockflow server init, or the user-data of server cloud-init) also writes the files below. server uninstall leaves them in place, as it does the boot units above. It removes the filter's rules, so the filter unit then loads nothing.

PathWhat it is
/home/<user>/.ssh/authorized_keysThe deploy user's key line, with restrict,port-forwarding,pty (and from= with --from); 0600, in a 0700 directory
/etc/ssh/sshd_config.d/01-dockflow.confWith --harden-ssh: the SSH hardening drop-in. Servers prepared by earlier versions have 50-dockflow.conf instead, left in place; remove it with sudo rm /etc/ssh/sshd_config.d/50-dockflow.conf && sudo systemctl reload ssh
/etc/docker/daemon.jsonOnly when Dockflow installed Docker and the file did not exist: the json-file log driver, 10 MB per file, 3 files
/etc/apt/keyrings/docker.asc, /etc/apt/sources.list.d/docker.listDocker's apt repository, when Dockflow installed Docker
/etc/nginx/conf.d/00-dockflow-default.confWith --nginx: the default server that refuses unknown host names (Debian's sites-enabled/default link is removed)
/etc/sudoers.d/dockflow-nginxWith --nginx: the sudo rules of the nginx plugin, 0440

server.json

Written by dockflow server init:

{
  "schema": 1,
  "name": "main",
  "edge": "external",
  "switch_image": "caddy:2.11.7@sha256:…",
  "switch_listen": "127.0.0.1:8080",
  "trusted_proxies": ["10.0.0.0/8"],
  "registry_image": "registry:3.1.2@sha256:…",
  "dockflow_version": "1.0.0-alpha.6",
  "watchdog_image": "sha256:…",
  "ssh_port": 22,
  "cloudflare_ranges": ["173.245.48.0/20", "…"],
  "filter_rules": "9f2c…"
}

Fields that are not always present:

  • acme_email and acme_ca are present when server init --acme-email or --acme-ca set them.
  • switch_listen and trusted_proxies are present with the external edge only.
  • switch_image is absent when the server runs no switch (--edge none), and registry_image when it runs no registry (--no-registry).
  • no_port_filter and no_watchdog record that server init was asked to leave the filter or the watchdog out. watchdog_image is absent without the watchdog.

filter_rules is the digest of the port filter's rules last applied, so a deploy that changes no port leaves the filter alone.

Locks

  • apps/<stack>/lock is an empty file. A run takes it with flock and holds it until the agent exits. If the agent dies, the kernel releases it. A second run fails with lock.held (exit code 5), or waits with --wait-lock.

  • apps/<stack>/lock.json names the holder, for the error message:

    { "schema": 1, "run": "01jbx6k3m8w0v7c9q2r5t4y6z8", "op": "deploy", "by": "alice@laptop", "ci": "github", "since": "2026-10-26T14:03:11Z" }

    ci is the CI provider, absent outside CI.

  • apps/<stack>/manual-lock.json exists while dockflow lock acquire holds the environment, with the same fields and the message of -m. Every run refuses to start while it exists, unless given --force. It never expires: dockflow lock release removes it.

  • apps/<stack>/backup.json exists while a backup or a restore has a service stopped or quiesced, or its volumes being replaced. The watchdog, or the next run that takes the lock, finishes it if the agent died: see Backups.

env.json and releases

{ "schema": 1, "last_seq": 42, "current": 42, "previous": 41, "degraded": false }

env.json points to the current and previous release.

Each releases/<seq>.json.gz is a gzipped JSON record of one release. It holds who made it and from which commit, its status, and the complete desired state of the app: every container's specification, with images pinned by digest. A rollback recreates containers from it. Values interpolated into a container's environment are part of that specification, as they are in docker inspect.

A release's status is applying while a run works on it, then active, then superseded when a newer one goes live. A failed run leaves rolled_back or failed, and a run whose agent died is marked abandoned by the next run.

After a successful run, the agent keeps the newest releases.keep releases that went live, and every release newer than the oldest of those. It deletes the other records. It then deletes the images, registry manifests and upload contents that no kept release uses.

Containers

NameWhat it isLifetime
dockflow-switchThe switch: Caddy, which routes traffic to your services (Switch and edges)Permanent, restart: unless-stopped. Absent with --edge none
dockflow-registryThe server-local registry that receives the images you buildPermanent, restart: unless-stopped. Absent with --no-registry
dockflow-watchdogThe watchdog: restarts the containers that stay unhealthy, brings apps back after a reboot, and finishes or rolls back a run whose agent died (Interrupted runs)Permanent, restart: unless-stopped, no network; it mounts the switch's admin volume to move routes back. Absent with --no-watchdog
dockflow-run-<run-id>The agent of one run (How it works)Exits when the run ends. The 10 most recent finished ones are kept for dockflow attach, older ones are removed by the next run
dockflow-upload-<run-id>-<n>A helper that writes an upload to its destinationRemoved as soon as it finishes
dockflow-filter-<run-id>, dockflow-probe-<run-id>Helpers on the host's network: one applies the port filter's rules, the other checks whether ports 80 and 443 are free (server check)Removed as soon as they finish
<stack>-<service>-<seq>-<n>The n-th container of a service, created by release <seq>: shop-production-web-42-1Until a later release replaces it. A service left unchanged by a deploy keeps the containers of an older release
container_nameA service with a container_name keeps that name, and always uses the stop-first strategyAs above
<stack>-<service>-<seq>-jobA job of release <seq>Removed when the job finishes
<stack>-<service>-run-<id>A one-off container of dockflow runRemoved when the command exits

The release number in a container's name is not padded: shop-production-web-42-1.

Images

ImageUse
dockflow/agent:<version>The agent, built on the server from the dockflow Linux binary of the CLI's version. Development builds use dockflow/agent:dev-<hash>
caddy:2.x.yThe switch and the servers of files routes: the release this version of Dockflow pins, by digest (--switch-image changes it)
registry:3.x.yThe registry: the release this version pins, by digest (--registry-image changes it)
restic/restic:0.x.yBackups: the release this version pins, by digest
127.0.0.1:5959/<app>/<service>:<version>Images built by Dockflow and pushed to the server's registry. Services run them pinned by digest
localhost/<app>/<service>:<tag>Images built on the server (build.builder: remote) when no external registry is configured
Your public or external-registry imagesPulled by the agent and pinned by digest

Labels

Every container, network and volume of an app carries Compose's own labels, so Docker tools see the environment as a Compose project:

LabelValue
com.docker.compose.project<stack>
com.docker.compose.serviceThe service name (containers)
com.docker.compose.container-number1…n (containers)
com.docker.compose.oneoffTrue for jobs, one-off containers, agents and helpers, False for service containers
com.docker.compose.networkThe network's key in Compose (networks)
com.docker.compose.volumeThe volume's key in Compose (volumes)

Dockflow's own labels:

LabelValue
dockflow.managedtrue on the containers Dockflow creates, and on the app networks and volumes it creates
dockflow.schema1 (containers). Informational: Dockflow never reads it
dockflow.kindservice, job, oneoff (dockflow run), run (an agent), helper, switch, registry or watchdog on containers; agent on the agent's image. Later versions may add kinds
dockflow.sharedtrue on the shared networks of the server
dockflow.releaseThe release that created the container
dockflow.hashA hash of the container's specification: an unchanged hash means an unchanged container
dockflow.opOn agent containers: the operation they run (deploy, rollback, …)

You can list an environment's containers with Docker itself:

docker ps -a --filter label=com.docker.compose.project=shop-production
docker compose -p shop-production ps

Use docker compose -p to look, not to change. A container started or recreated outside Dockflow is not in any release: the next deploy plans from what is actually running and may replace it.

Networks and volumes

App networks and volumes keep Compose's names: <stack>_default, <stack>_<network>, <stack>_<volume> (for example shop-production_pgdata). Networks and volumes marked external: true are used as they are, never created or removed.

  • The switch joins the networks of the services it routes to, so it reaches them by name.
  • An agent joins the app's networks during its run, to run internal health checks, and leaves them at the end.

Dockflow's own volumes:

VolumeContent
dockflow-switch-dataCaddy's data: TLS certificates and their keys
dockflow-switch-configCaddy's saved configuration
dockflow-switch-adminThe switch's admin socket, mounted only by the switch and the agents, at /run/dockflow-switch
dockflow-registry-dataThe registry's storage
dockflow-backupsThe restic repository of the backups, when backups sets no off-site one
dockflow-backups-cacherestic's cache

Ports

PortWhoWhen
80/tcp, 443/tcp, 443/udp on every addressThe switchThe builtin edge (default): HTTP, HTTPS and HTTP/3
127.0.0.1:8080/tcp, or the address given to --switch-listenThe switchThe external edge, behind your own proxy
127.0.0.1:5959/tcpThe registryUnless --no-registry. Loopback only: the CLI reaches it through the SSH connection
SSH (22 by default)sshdAlways: every command connects over SSH
The ports your services publishYour containersAs declared in Compose ports:. From outside, only those listed under firewall.public_ports, unless the server runs without the port filter

The switch's admin API listens on no port at all, only on a unix socket.

Removing everything by hand

dockflow destroy removes one environment: its containers, routes, networks and state, and with --volumes its data. dockflow server uninstall removes Dockflow from a server entirely, in one command.

To do the same by hand:

  1. Remove every environment, from your project directory, for each environment:

    dockflow destroy production

    Add --volumes to delete the environment's volumes and their data too.

  2. On the server, remove the containers Dockflow manages that are left (the switch, the registry, the finished agents, and anything of an environment you can no longer destroy from its project):

    docker ps -aq --filter label=dockflow.managed=true | xargs -r docker rm -f
  3. Remove the networks it created:

    docker network ls -q --filter label=dockflow.managed=true | xargs -r docker network rm
  4. Remove the switch's and the registry's volumes:

    docker volume rm dockflow-switch-data dockflow-switch-config dockflow-switch-admin dockflow-registry-data dockflow-backups dockflow-backups-cache
  5. Remove the images:

    docker images -q dockflow/agent | xargs -r docker rmi
    docker images -q --filter reference='127.0.0.1:5959/*/*' | xargs -r docker rmi
    docker image prune -a

    The last command removes every image no container uses, caddy and registry included; skip it if the server runs other things.

  6. Remove the state:

    sudo rm -rf /var/lib/dockflow
  7. Remove the port filter, unless the server was prepared with --no-port-filter:

    sudo systemctl disable --now dockflow-filter
    sudo rm -f /etc/systemd/system/dockflow-filter.service
    sudo rm -rf /etc/dockflow
    sudo iptables -D DOCKER-USER -j DOCKFLOW-FILTER && sudo iptables -F DOCKFLOW-FILTER && sudo iptables -X DOCKFLOW-FILTER
    sudo ip6tables -D DOCKER-USER -j DOCKFLOW-FILTER && sudo ip6tables -F DOCKFLOW-FILTER && sudo ip6tables -X DOCKFLOW-FILTER
    sudo iptables -D INPUT -j DOCKFLOW-INPUT && sudo iptables -F DOCKFLOW-INPUT && sudo iptables -X DOCKFLOW-INPUT
    sudo ip6tables -D INPUT -j DOCKFLOW-INPUT && sudo ip6tables -F DOCKFLOW-INPUT && sudo ip6tables -X DOCKFLOW-INPUT
  8. Remove the Cloudflare refresh and the files of server init you no longer want (see above):

    sudo systemctl disable --now dockflow-cloudflare.timer
    sudo rm -f /etc/systemd/system/dockflow-cloudflare.timer /etc/systemd/system/dockflow-cloudflare.service /usr/local/libexec/dockflow-cloudflare-refresh
    sudo rm -f /etc/ssh/sshd_config.d/01-dockflow.conf /etc/ssh/sshd_config.d/50-dockflow.conf /etc/sudoers.d/dockflow-nginx
    sudo systemctl daemon-reload

App volumes hold your data and are not removed by the steps above unless you used destroy --volumes. To remove the ones left, docker volume ls --filter label=dockflow.managed=true lists them; docker volume rm deletes them for good. Back them up first.

Deleting /var/lib/dockflow while the containers still run does not stop your services, but Dockflow loses their release history: there is nothing left to roll back to.

Edit on GitHub

On this page