Exit codes and errors
Every exit code of the dockflow CLI, the format of its errors, and every error and warning code, with what to do about each.
Every dockflow command ends with an exit code that tells a script or a CI job what happened, without parsing text. When a command fails, it also prints an error with a stable code and a hint that says what to do next.
This page lists every exit code, the error format, every error code grouped by exit code, and the warning codes. To act on a failure in a script, check the exit code first, then the error code for detail.
Exit codes are fixed for the whole of 1.x: a new one only comes with a major release. New error and warning codes may appear in a minor release, each mapped to an existing exit code.
Exit codes
| Code | Name | Meaning | Typical causes |
|---|---|---|---|
0 | OK | Success, including a plan with nothing to do | A deploy that went live, a deploy with nothing to change, a read command |
1 | Internal | Unexpected error. This is a bug: please report it | The agent stopped before finishing, a corrupt state file, the switch's admin API not answering |
2 | Usage | Wrong usage | Unknown command or flag, a bad argument, an unknown run id for attach, a CI tag whose suffix names no environment |
3 | Config | Invalid configuration | An error in dockflow.yml or Compose, a variable with no value, an unreadable .env.dockflow, a key from a later Dockflow, an SSH key protected by a passphrase |
4 | Connect | Cannot connect | SSH unreachable or refused, unknown or changed host key, Docker API unreachable, Docker older than 27, the agent binary cannot be downloaded |
5 | Locked | Another run holds the lock | A deploy already running on the same environment, a manual lock taken with dockflow lock acquire |
6 | Plan | The plan is refused; nothing was changed | A saved plan that is stale, a domain routed twice, an image the server cannot pull, routes on a server without a switch, nothing to roll back to |
7 | Build | Build or push failed; nothing was changed on the server | A failing Dockerfile, buildx or Podman missing, a registry that refuses the push |
8 | Rolled back | The run failed and was rolled back: the previous release is running | A service that does not become healthy, a failed health check, a failed job, a fatal after_deploy hook |
9 | Degraded | The run failed and was not rolled back: the environment is degraded | A failed health check with on_failure: fail or --no-rollback, a rollback that failed too; dockflow diagnose found a problem |
10 | Lock lost | The lock was lost during the run | Reserved: the lock is held until the agent exits, so this version never reports it |
11 | Hook | A fatal hook failed before anything changed | A fatal before_build, after_build or before_deploy hook that exited with a non-zero code |
12 | Backup | A backup failed | A dump that exits with an error, a repository that cannot be opened or fails its check, a service that is not running |
13 | State | The server's state needs another Dockflow; nothing was changed | A newer Dockflow changed this environment or this server, and recorded something this version does not know: a CI pinned to an older version, after a deploy with a newer one |
130 | Interrupted or declined | The run was cancelled and rolled back, or never started | Ctrl+C or dockflow cancel (run.cancelled), answering no at a confirmation prompt (run.declined), detaching with a second Ctrl+C (run.detached). See cancelling |
dockflow exec, dockflow run and dockflow ssh exit with the exit code of the command they ran. When they cannot start it, they use the codes above.
In CI, two runs that may overlap (a push to main and a release tag) do not need to fail with code 5: dockflow deploy --wait-lock 10m waits for the other run's lock instead.
Error format
Every error Dockflow understands carries three parts:
| Part | Description |
|---|---|
code | A stable, dotted identifier, such as lock.held or ssh.host_key_mismatch. Scripts may match on it; it does not change between versions. A code a script does not know is handled by its exit code. |
message | What went wrong, in one sentence. Secret values in it are replaced by ***. |
hint | What to do about it. Not every error has one. |
Human output
On a terminal, the error goes to standard error, with the hint on a second line:
error: Compose: variable API_KEY is not defined: error while interpolating services.web.environment.API_KEY: variable API_KEY is not defined
hint: define it under environments.<env>.vars or as a secret, or write ${API_KEY:-default}JSON output
With --json, an error found by the CLI is printed on standard output as one JSON document, with the exit code:
{"error":{"code":"config.invalid","message":"Compose: variable API_KEY is not defined: error while interpolating services.web.environment.API_KEY: variable API_KEY is not defined","hint":"define it under environments.<env>.vars or as a secret, or write ${API_KEY:-default}"},"exit_code":3}The hint field is left out when the error has none.
When the error happens during the agent's run on the server, it is already in the event stream, in the error field of the run.finished event. The CLI does not print it a second time.
Errors from the argument parser itself (an unknown command or flag) are printed as error: … on standard error, also with --json, and exit with code 2.
Error codes
Each error code below maps to the exit code given in its row. A few errors use a generic code for their class, and their message says what went wrong: config.invalid (3), usage.invalid (2), connect.failed (4) and internal (1). A script that meets a code it does not know can act on the exit code.
Configuration and usage
| Code | Exit | What it means | What to do |
|---|---|---|---|
config.invalid | 3 | dockflow.yml, Compose, a secret or a variable is wrong. The message names the file and the key. | Fix what the message names. A missing variable needs a value in vars, a secret, or a default (${X:-default}). Run dockflow validate to check without a server. |
config.newer | 3 | dockflow.yml is in a newer format version, or its requires names a later Dockflow than this one. | Upgrade Dockflow. |
secrets.file | 3 | The environment's SOPS file cannot be decrypted. | Give the age key: SOPS_AGE_KEY in CI, or the key file SOPS reads on your machine. |
usage.invalid | 2 | A bad argument or flag, or an environment prefix that matches several environments. | Check dockflow <command> --help. |
init.exists | 2 | dockflow init would replace a file that exists. | Run it with --force, or move the file. |
run.unknown | 2 | dockflow attach was given a run that is not on the server. | Only the 10 most recent finished runs are kept. |
Connecting and provisioning
| Code | Exit | What it means | What to do |
|---|---|---|---|
ssh.host_key_unknown | 4 | The server's host key is neither pinned in the connection string nor in ~/.ssh/known_hosts. Dockflow never trusts a key on first use. | Check the fingerprint on the server with ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub, then pin it (host_key: in dockflow.yml, or --host-key for dockflow server connection). |
ssh.host_key_mismatch | 4 | The server's host key is not the pinned one. Either the server was reinstalled, or something sits between you and it. | Verify the new key on the server itself, then print the connection string again with dockflow server connection and update the secret. Do not update it without checking. |
connect.failed | 4 | The server cannot be reached, SSH authentication failed, or the Docker API does not answer. | Check the host, port, user and key. Check that Docker runs on the server and that the user can use its socket. |
server.docker_too_old | 4 | The server runs Docker older than 27. | Upgrade Docker on the server. |
agent.no_binary | 4 | A development build of the CLI has no release to download the agent binary from: it does not run on Linux with the server's architecture, or it is linked dynamically (built with cgo), and the agent's image holds no C library. | Build it with CGO_ENABLED=0 (make build does), or set DOCKFLOW_AGENT_BINARY to a static dockflow binary for the server's architecture. |
agent.download_failed | 4 | The agent binary for the server could not be downloaded from the release. | Check that this machine reaches GitHub (or your mirror in DOCKFLOW_RELEASES_URL), or set DOCKFLOW_AGENT_BINARY. |
agent.checksum_mismatch | 4 | The downloaded agent binary does not match the checksum the CLI was built with (the release's SHA256SUMS for a CLI built without one). Nothing was sent to the server. | Try again. Report it if it persists. |
provision.sudo | 4 | server init connected as a user that cannot use sudo without a password. | Connect as root, or give that user passwordless sudo for the time of server init. |
provision.no_docker | 4 | Docker is not installed on the server, and --skip-docker-install was given. | Install Docker 27 or later, or drop --skip-docker-install. |
provision.unsupported_os | 4 | Docker is not installed, and the server is not Debian or Ubuntu, where Dockflow installs it. | Install Docker 27 or later yourself, then run server init again. |
provision.docker_too_old | 4 | The server's Docker is older than 27, or not running. | Upgrade Docker Engine to 27 or later and start it. |
provision.no_nginx | 4 | --nginx was given, nginx is not installed, and the server is not Debian or Ubuntu, where Dockflow installs it. | Install nginx yourself, then run server init again. |
provision.docker_key | 4 | The key download.docker.com gave is not Docker's signing key (fingerprint 9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88), or comes with another key. Docker is not installed. | Something between the server and download.docker.com changed the key: install Docker yourself from a source you trust, then run server init again. |
provision.no_ssh_server | 4 | server init without a target, on the server itself, found no SSH host key: no SSH server is installed. | Install and start OpenSSH, then run server init again. |
provision.failed | 4 | A step of the provisioning script failed. | The output above says which step. Every step can be run again safely. |
Locks
| Code | Exit | What it means | What to do |
|---|---|---|---|
lock.held | 5 | Another run (named in the message, with who started it and when) is working on this environment. | Wait for it, or retry with --wait-lock <duration>. |
lock.manual | 5 | Someone holds the environment's lock by hand, with dockflow lock acquire. The message says who and why. | Run dockflow lock release when the maintenance is over, or deploy anyway with --force. |
Planning
Nothing was changed on the server.
| Code | Exit | What it means | What to do |
|---|---|---|---|
plan.stale | 6 | The server changed between the plan you confirmed and the apply. | Run the command again to compute a new plan. |
plan.image_missing | 6 | The server cannot pull an image, or it is absent with pull_policy: never. | Check the image name, and that the server can reach the registry and is logged in to it. |
plan.pull_policy | 6 | A service's pull_policy is one the server cannot apply. | Use missing, always or never. |
plan.caddy | 6 | A route's raw caddy directives do not parse. | Fix them: they are Caddyfile directives, as inside a site block. |
plan.domain_conflict | 6 | A domain is routed by two app environments, at any paths, or a domain and path by two services. Domains are compared lower-cased without a trailing dot, paths without a trailing slash. | A domain belongs to one app environment, and a domain and path to one service: change one of them. |
plan.domain_invalid | 6 | A route stored on the server has a domain that is not a host name. | Deploy the app that owns it again, with a valid domain. |
plan.no_switch | 6 | The app declares routes, and the server has no switch. | Run dockflow server init. |
plan.routes_without_switch | 6 | The server was set up with --edge none, and the app declares routes. | Publish ports instead, or run dockflow server init --edge builtin. |
server.routes_in_use | 6 | --edge none was asked for, and routes still go through the switch. | Remove the routes of the apps on this server first. |
plan.network_missing, plan.volume_missing | 6 | A network or volume marked external: true does not exist on the server. | For a shared network, run the dockflow server network create the hint gives; otherwise create it on the server, or remove external: true. |
plan.volume_driver | 6 | A volume's driver is not a volume plugin the server has. | Run dockflow server plugin install <plugin> --alias <driver>. |
plan.network_foreign | 6 | A network of the app's name exists on the server but belongs to something else: a Docker Swarm stack, another Compose project, or a network created by hand. Dockflow never takes it over. | Remove it once nothing uses it (docker stack rm for a Swarm stack), or give the network another name in compose.yml. |
plan.only_dependency | 6 | --only names a service that depends on one that is not deployed. | Add that service to --only. |
plan.dependency_removed | 6 | destroy --only would remove a service that another one depends on. | Destroy them together. |
plan.stateful_replicas, plan.container_name_replicas | 6 | dockflow scale was asked for several replicas of a service that runs once. | Stateful services and services with a container_name keep one container. |
secret.not_tmpfs | 6 | /run/dockflow on the server is not a tmpfs, so Compose secrets, or the backups' password and storage credentials, cannot be written to memory. | Mount a tmpfs on /run/dockflow (systemd mounts one on /run). dockflow server check checks it. |
secret.missing, config.missing | 6 | A rollback needs a secret or config file that is gone from the server (a reboot empties the tmpfs), and this run does not have its value. | Deploy the current version: the CLI sends the values it has. |
network.in_use, plugin.in_use | 6 | A shared network has containers, or a volume plugin has volumes. | Remove them from the apps and deploy, or destroy the environments that use them, first. |
network.exists, network.not_shared, plugin.exists, plugin.install | 6 | The network's name belongs to a network that is not shared, the plugin's name is taken, or the plugin cannot be pulled or enabled. | Choose another name or --alias; check the plugin's name and that the server reaches its registry. |
rollback.no_previous, rollback.unknown_release | 6 | There is no release to roll back to, or the one asked for is no longer kept. | List the releases with dockflow releases. |
rollback.current | 6 | The release or version asked for is the current one. | Nothing to do, or pick another with --to. |
rollback.not_deployable | 6 | The release asked for never went live: it was rolled back, failed or abandoned. | Pick a release whose status is active or superseded in dockflow releases. |
env.never_deployed | 6 | The command needs a deployed release, and the environment has none. | Deploy it first. |
Building and pushing
Nothing was changed on the server.
| Code | Exit | What it means | What to do |
|---|---|---|---|
build.failed | 7 | The image build failed. | The build output above the error says why. With -v, every build line is shown. |
build.no_buildx | 7 | docker buildx cannot run on this machine. | Install Docker with the buildx plugin. |
build.no_builder | 7 | The buildx builder cannot export images. | Enable the containerd image store, or create a docker-container builder. |
build.no_podman | 7 | build.engine: podman is set, and Podman cannot run. | Install Podman on this machine. |
build.not_pushed | 7 | deploy --skip-build found no image of a service for this version in the registry. | Build and push it first with dockflow build --push, for the same version, or deploy without --skip-build. |
push.failed | 7 | An image cannot be pushed to the server's registry or to your external registry. | Check that the registry is reachable and that the credentials can push. |
During the rollout
| Code | Exit | What it means | What to do |
|---|---|---|---|
health.failed | 8 | A service's containers did not become healthy: they crashed, exited, failed their Docker health check, or did not answer in time. The previous release was restored. | Read the service's logs with dockflow logs <env> <service>, and run dockflow diagnose. |
health.check_failed | 8 or 9 | An HTTP health check did not get the expected status after its retries. | Check the URL and the expected status. on_failure decides whether the deploy rolls back (8) or stays in place (9). |
job.failed, job.timeout | 8 | A job exited with a non-zero code, or did not finish within the deploy timeout. | Its output is in the stream above. If the job needs longer, raise environments.<env>.deploy.timeout. |
hook.failed | 11 or 8 | A fatal hook failed: before anything changed (11), or an after_deploy hook once the release went live (8, and the previous release is restored). A non-fatal hook only prints a warning. | Its output is in the stream above. |
upload.failed | 8 | An upload could not be written on the server. | Check the destination path and its permissions. |
engine.port_in_use | 8 or 1 | A container cannot start because a host port it publishes is in use, by another container or a process of the server. In a deploy, the previous release was restored (8). | Free the port on the server, or publish another one. |
state.commit_failed | 8 | Everything was ready, but the release could not be recorded as live on the server (a full disk, an I/O error), so the previous release was restored. | Check the disk of the server (df -h /var/lib/dockflow), then deploy again. |
rollback.failed | 9 | The run failed, and restoring the previous release failed too. The environment is degraded. | Fix the cause and deploy again. dockflow status and dockflow diagnose show the state. |
run.failed | the run's | The agent's run failed without an error of its own; run.finished and the exit code say how it ended. | Read the events above run.finished. |
Exit code 8 means the deploy failed but your users still see the previous release. Exit code 9 means they may not: the environment is left as the failed run put it. Treat 9 as the one that pages someone.
Cancelling
Exit code 130 covers three cases, told apart by the error code.
| Code | Exit | What it means | What to do |
|---|---|---|---|
run.cancelled | 130 | The run was cancelled (Ctrl+C or dockflow cancel) and rolled back. | Nothing: the previous release is running. |
run.declined | 130 | You answered no at the confirmation prompt. | Nothing was changed. |
run.detached | 130 | A second Ctrl+C detached the CLI while the agent rolls back on the server. | Follow it with dockflow attach <env> <run>. |
Checks
| Code | Exit | What it means | What to do |
|---|---|---|---|
check.failed | 9 | dockflow server check found a failure on the server. | Each failed check says what to do. |
diagnose.findings | 9 | dockflow diagnose found problems; the message says how many. | Each finding is listed above it. |
Backups
| Code | Exit | What it means | What to do |
|---|---|---|---|
backup.none | 2 | The service named does not exist in the current release, or declares no backup. | Set x-dockflow.backup on it, and deploy. |
backup.failed | 12 | The backup of a service failed; its reason is in a warning above. A failed dump keeps no snapshot. | Read the dump's error. |
backup.not_running | 12 | The service has no running container to dump. | Start it with dockflow restart. |
backup.repository | 12 | The repository cannot be opened or listed. | Check backups.repository, the credentials in backups.env, and the password. |
backup.snapshot | 2 or 12 | No snapshot to restore, or none matches --snapshot. | dockflow backup list shows the snapshots. |
backup.restore_failed | 12 | The restore failed; the message gives the restore command's error. A Postgres restore is one transaction: nothing of it stays. | Read the error, fix the target, and restore again. |
backup.retention | 12 | backup prune could not apply a retention. | Read restic's error in the message. |
backup.check | 12 | The repository failed restic check. | Run restic check on the repository to see its errors. |
Server state
Nothing was changed.
| Code | Exit | What it means | What to do |
|---|---|---|---|
state.newer | 13 | A newer Dockflow changed this environment (or this server) and recorded fields this version does not know. It can read the state, not change it. | Upgrade Dockflow to the version the server was last changed with, in CI too. |
state.schema | 13 | The server's state is in another format version, written by another major release of Dockflow. | Use the Dockflow that wrote it, or a later one of the same major. |
Unexpected errors
These exit with code 1. Most come from the server's Docker or files, not from your project. When the cause is not clear from the message, report it.
| Code | Exit | What it means | What to do |
|---|---|---|---|
internal | 1 | An error Dockflow did not expect. | Report it, with the output of the command run with -v. |
agent.died | 1 | The agent stopped before finishing. | The watchdog, or else the next run, finds the unfinished release: it restores the previous release, or completes the new one if it was already live. dockflow releases shows which. |
agent.protocol | 1 | The agent's last event cannot be read. | Report it. Check that the agent's output is not cut off on the server (disk full, killed container). |
engine.api | 1 | The server's Docker refused a request; the message says which, with Docker's error. | Read Docker's error. Check the server's disk and the Docker daemon. |
state.io, state.corrupt | 1 | A file under /var/lib/dockflow cannot be read or written, or is not a valid state file. | Check the server's disk. A corrupt file is named in the message. |
state.missing_release | 1 | The environment's current release record is missing from the server. | Deploy again to write a new release. |
switch.unreachable | 1 | The switch's admin API does not answer. | Check the dockflow-switch container on the server, or run dockflow server init again. |
switch.config_refused | 1 | The switch refused the configuration Dockflow sent it. | Report it, with the message. |
server.start_failed | 1 | The switch, the registry or the watchdog cannot start. | The hint says what to check, usually a port already in use. |
filter.no_iptables, filter.failed | 1 | The port filter cannot be applied: iptables or ip6tables is missing on the server, or refused the rules. | Install iptables on the server, or run dockflow server init --no-port-filter. |
filter.inert | 1 | Docker does not send forwarded traffic through DOCKER-USER: with its nftables firewall backend, or "iptables": false, the port filter would let everything in. | Use Docker's iptables backend, or run dockflow server init --no-port-filter and filter the ports yourself. |
filter.ipv6 | 1 | The server has an IPv6 address, but the port filter's IPv6 rules cannot be applied: published ports would stay open over IPv6. | Install ip6tables on the server, or turn IPv6 off. |
prune.registry | 1 | The server-local registry's garbage collection failed. | Read the registry's error in the message. |
Two codes never reach you as errors:
run.abandonedis recorded in theerrorof a release whose agent died, and shown bydockflow releases;command.exitcarries the exit code of the commandexec,runorsshran.
Warning codes
Warnings appear as warning events with a code. They never change the exit code. New codes may appear in a minor release.
| Code | Meaning |
|---|---|
compose.warning | A Compose key that has no effect on one server, or that Dockflow ignores (the message says which) |
hook.failed | A hook that is not fatal failed |
health.check_skipped | An internal HTTP check was skipped, because the service is on no network of the app |
notify.failed | A webhook could not be delivered. Webhooks never change the outcome of a run. |
run.abandoned, run.leftover, run.restarted | An earlier run stopped before finishing; this run marked its release abandoned, removed its leftover containers or restarted the containers it had stopped |
run.unfinished | The latest release failed and was left in place (on_failure: fail, --no-rollback): this run rolls it back to the current release before its plan, or, from dockflow plan, the next run will |
cleanup.failed, destroy.network, state.write_failed | A cleanup step (old containers, images, registry manifests, release files, networks) or a state write failed; it does not change the outcome of the run |
backup.none | backup run found no service of the current release that declares a backup: nothing was backed up |
backup.password_generated | The backups' password was generated and stored on the server: keep a copy elsewhere |
backup.resume | A volume backup could not start the service again, or run its post command |
agent.signal_failed | The CLI could not forward Ctrl+C to the agent |
upgrade.pull | server upgrade could not pull an image; the server keeps the one it has |
uninstall.network, uninstall.volume, uninstall.filter | server uninstall could not remove a network, a volume or the port filter's rules; the message says which |
backup.failed, backup.retention | In backup run, the backup or the retention of one service failed (level error); the others go on, and the command exits 12 |
secret.copy | The copy of a secret kept on the server for rollbacks could not be written: a rollback after a reboot will need the value again |
filter.failed | The port filter could not be applied after the run; the message says why |
cloudflare.ranges | Cloudflare's IP ranges could not be downloaded; the previous ones, or this version's own list, are used |
plan.no_limits | The server runs other app environments, and services of this one set no deploy.resources.limits |
notify.detached | deploy --detach sends no webhook: the CLI sends them when the run ends |
watchdog.failed | The watchdog could not check or restart a container; in the watchdog's own output |
watchdog.recovered | The agent of a run stopped before the run finished (killed, out of memory, Docker restarted), and the watchdog recovered the environment at once, as the next run would have; in the watchdog's own output |