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 runIn these paths:
<stack>is<app>-<env>, for exampleshop-production;<seq>is the release number, written with 6 digits in file names (000042.json.gz);<repository>islocalfor thedockflow-backupsvolume, 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
0700and its files0600: 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.
| Path | What it is |
|---|---|
/home/<user>/.ssh/authorized_keys | The 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.conf | With --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.json | Only 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.list | Docker's apt repository, when Dockflow installed Docker |
/etc/nginx/conf.d/00-dockflow-default.conf | With --nginx: the default server that refuses unknown host names (Debian's sites-enabled/default link is removed) |
/etc/sudoers.d/dockflow-nginx | With --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_emailandacme_caare present whenserver init --acme-emailor--acme-caset them.switch_listenandtrusted_proxiesare present with theexternaledge only.switch_imageis absent when the server runs no switch (--edge none), andregistry_imagewhen it runs no registry (--no-registry).no_port_filterandno_watchdogrecord thatserver initwas asked to leave the filter or the watchdog out.watchdog_imageis 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>/lockis an empty file. A run takes it withflockand holds it until the agent exits. If the agent dies, the kernel releases it. A second run fails withlock.held(exit code 5), or waits with--wait-lock. -
apps/<stack>/lock.jsonnames the holder, for the error message:{ "schema": 1, "run": "01jbx6k3m8w0v7c9q2r5t4y6z8", "op": "deploy", "by": "alice@laptop", "ci": "github", "since": "2026-10-26T14:03:11Z" }ciis the CI provider, absent outside CI. -
apps/<stack>/manual-lock.jsonexists whiledockflow lock acquireholds the environment, with the same fields and themessageof-m. Every run refuses to start while it exists, unless given--force. It never expires:dockflow lock releaseremoves it. -
apps/<stack>/backup.jsonexists 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
| Name | What it is | Lifetime |
|---|---|---|
dockflow-switch | The switch: Caddy, which routes traffic to your services (Switch and edges) | Permanent, restart: unless-stopped. Absent with --edge none |
dockflow-registry | The server-local registry that receives the images you build | Permanent, restart: unless-stopped. Absent with --no-registry |
dockflow-watchdog | The 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 destination | Removed 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-1 | Until a later release replaces it. A service left unchanged by a deploy keeps the containers of an older release |
container_name | A service with a container_name keeps that name, and always uses the stop-first strategy | As above |
<stack>-<service>-<seq>-job | A job of release <seq> | Removed when the job finishes |
<stack>-<service>-run-<id> | A one-off container of dockflow run | Removed when the command exits |
The release number in a container's name is not padded: shop-production-web-42-1.
Images
| Image | Use |
|---|---|
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.y | The switch and the servers of files routes: the release this version of Dockflow pins, by digest (--switch-image changes it) |
registry:3.x.y | The registry: the release this version pins, by digest (--registry-image changes it) |
restic/restic:0.x.y | Backups: 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 images | Pulled 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:
| Label | Value |
|---|---|
com.docker.compose.project | <stack> |
com.docker.compose.service | The service name (containers) |
com.docker.compose.container-number | 1…n (containers) |
com.docker.compose.oneoff | True for jobs, one-off containers, agents and helpers, False for service containers |
com.docker.compose.network | The network's key in Compose (networks) |
com.docker.compose.volume | The volume's key in Compose (volumes) |
Dockflow's own labels:
| Label | Value |
|---|---|
dockflow.managed | true on the containers Dockflow creates, and on the app networks and volumes it creates |
dockflow.schema | 1 (containers). Informational: Dockflow never reads it |
dockflow.kind | service, 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.shared | true on the shared networks of the server |
dockflow.release | The release that created the container |
dockflow.hash | A hash of the container's specification: an unchanged hash means an unchanged container |
dockflow.op | On 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 psUse 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:
| Volume | Content |
|---|---|
dockflow-switch-data | Caddy's data: TLS certificates and their keys |
dockflow-switch-config | Caddy's saved configuration |
dockflow-switch-admin | The switch's admin socket, mounted only by the switch and the agents, at /run/dockflow-switch |
dockflow-registry-data | The registry's storage |
dockflow-backups | The restic repository of the backups, when backups sets no off-site one |
dockflow-backups-cache | restic's cache |
Ports
| Port | Who | When |
|---|---|---|
80/tcp, 443/tcp, 443/udp on every address | The switch | The builtin edge (default): HTTP, HTTPS and HTTP/3 |
127.0.0.1:8080/tcp, or the address given to --switch-listen | The switch | The external edge, behind your own proxy |
127.0.0.1:5959/tcp | The registry | Unless --no-registry. Loopback only: the CLI reaches it through the SSH connection |
SSH (22 by default) | sshd | Always: every command connects over SSH |
| The ports your services publish | Your containers | As 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:
-
Remove every environment, from your project directory, for each environment:
dockflow destroy productionAdd
--volumesto delete the environment's volumes and their data too. -
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 -
Remove the networks it created:
docker network ls -q --filter label=dockflow.managed=true | xargs -r docker network rm -
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 -
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 -aThe last command removes every image no container uses,
caddyandregistryincluded; skip it if the server runs other things. -
Remove the state:
sudo rm -rf /var/lib/dockflow -
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 -
Remove the Cloudflare refresh and the files of
server inityou 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.
Environment variables
Every environment variable dockflow reads (secrets, SSH keys, agent download, CI detection, the installer) and the built-in variables it provides to Compose, templates and hooks.
Security model
What Dockflow protects and what it does not - SSH host keys, connection strings, Docker access, the port filter, secrets, the server's state, verified downloads and signed webhooks.