Dockflow

Releases and rollbacks

What a release records, how a failed deploy rolls back on its own, what dockflow rollback restores, and how an interrupted run is repaired.

Every deploy that changes something creates a release: a numbered record, kept on the server, of exactly what the app ran. A failed deploy goes back to the previous release on its own, and dockflow rollback re-applies any release still kept, without a rebuild.

What a release records

Releases are numbered per environment: 1, 2, 3, and so on, never reused. Each one is a file on the server, /var/lib/dockflow/apps/<app>-<env>/releases/<seq>.json.gz. The agent writes it, and never changes it once the run is over. It records:

  • the desired app: every service with its full container spec, as sent to Docker (image pinned by digest, environment, ports, mounts, networks, healthcheck, restart policy), plus its strategy, replicas and routes;
  • the uploads: each destination with the digest of its file manifest;
  • which release serves each service: a service that a deploy left unchanged keeps the containers of an older release;
  • its version label, the git commit and branch, who deployed it, from which machine or CI provider, and when;
  • what made it: deploy, rollback, restart, stop, scale or destroy (of some services);
  • its status, and the error when the run failed.

Values interpolated into the environment of a container, secrets included, are part of its spec, as they are in docker inspect. That is why release files, like all of /var/lib/dockflow, are readable by root only.

Statuses

StatusMeaning
applyingThe run is in progress
activeThe current release
supersededA release that was active before the current one
rolled_backThe run failed and the previous release was restored
failedThe run failed and was left in place (on_failure: fail, --no-rollback), or its rollback failed
abandonedThe agent stopped before the release went live; found by the watchdog or the next run, which restored the previous release

Only active and superseded releases went live, so only they can be restored with dockflow rollback.

Version labels

Besides its number, a release carries a free label, version. dockflow releases and dockflow version <env> show it, and the configuration can use it as ${DOCKFLOW_VERSION}. It is the first of:

  1. --version <label> on dockflow deploy;
  2. in CI, for a run started by a tag, the tag without its environment suffix: 1.2.0 for the tags 1.2.0 and 1.2.0-staging;
  3. the short git commit, suffixed -dirty when the working tree has uncommitted changes;
  4. outside git, the time of the deploy in UTC, such as 20261026-140311.

Two releases may share a label (a redeploy of the same commit after a configuration change); the number tells them apart.

Retention

After each successful run, the agent keeps:

  • the newest releases.keep releases that went live (active or superseded), 10 by default;
  • every release newer than the oldest of those, whatever its status.

It deletes the other release files. Then it deletes the images, the server-local registry's manifests and the previous upload contents that no kept release uses. So every kept release can be restored from what is on the server.

dockflow.yml
version: 1
app: shop

releases:
  keep: 5          # at least 2

environments:
  production: {}

The registry's storage is freed separately by dockflow prune --registry. See Cleanup.

When a deploy fails

A deploy keeps the old containers until it commits. New containers start next to them (or after them, with stop-first). Until the release becomes active, the old containers are only stopped, never removed. So when something fails before the commit, the agent:

  1. starts the old containers again if they were stopped, and waits until they are healthy;
  2. points the switch back at them, which takes about a second for routed services;
  3. puts back the files uploads had changed;
  4. removes the new containers.

The release ends rolled_back, the previous one is still active, and the command exits with code 8. after_failure hooks run, and webhooks are sent.

What counts as a failure:

  • a new container exits, restarts three times (a crash loop), becomes unhealthy, or is not healthy within its timeout (Rollouts);
  • a published port is already in use;
  • a before or after job exits with a non-zero code, or runs longer than its timeout;
  • a fatal hook fails;
  • an internal or public HTTP check fails, unless on_failure says otherwise (see below);
  • the release cannot be recorded as live on the server, at the very end (a full disk): state.commit_failed;
  • the run is cancelled with Ctrl+C or dockflow cancel: it rolls back and exits with code 130.

A deploy reports a failure only once the previous release serves again. A deploy reported failed never leaves the new release serving.

The other way round, once the release is recorded as live (one write, the commit), it stays live. If the agent cannot finish what is left (marking the previous release superseded, removing the old containers), the next run completes it.

Some failures happen before anything changed on the server. They stop the run with nothing to undo:

  • a build that fails (exit code 7);
  • a fatal before_build or before_deploy hook (exit code 11);
  • a lock held by another run (exit code 5).

If the rollback itself fails, the release ends failed, the environment is marked degraded, and the command exits with code 9. dockflow status shows the flag until a run succeeds.

A fatal after_deploy hook, on your machine or the server, fails after the release became active. The CLI then runs dockflow rollback to the previous release.

on_failure

on_failure decides what a failed HTTP check does: the internal check of a service (x-dockflow.health.http) or a public endpoint (health.endpoints). Set it in dockflow.yml, or per environment.

ValueA failed HTTP check
rollback (default)Rolls the whole app back to the previous release, as any other failure
failLeaves the run where it stopped, with the release marked failed; the environment is degraded; exit code 9
ignoreIs reported as a warning; the deploy goes on and the release becomes active
dockflow.yml
version: 1
app: shop

on_failure: rollback

environments:
  production: {}
  staging:
    on_failure: ignore

fail is for looking at a failed release, not for keeping it. The release stays in place only until the next run that takes the environment's lock:

  • That run, whatever it is (a deploy, a rollback, but also a restart or a scale), first rolls the failed release back to the current one, then plans from it.
  • dockflow plan warns about it (run.unfinished).
  • The watchdog leaves it alone.

To keep the new version, fix the check and deploy again.

A failure of the rollout itself always rolls back, whatever on_failure says: a container that crashes, stays unhealthy or times out, or a failed job. A service left half-switched is never useful.

dockflow deploy --no-rollback behaves as on_failure: fail for one run, to debug a failing check. See Rollouts.

dockflow rollback

dockflow rollback production                       # the previous release
dockflow rollback production --to 41               # a given release
dockflow rollback production --to 1.4.0            # the newest release of a version
dockflow rollback production --to 41 --service web # only web, as release 41 had it

--to takes either:

  • a release number, as listed by dockflow releases;
  • or a version label, for the newest release of that version that went live.

The target must be a release that went live (active or superseded), still kept, and not the current one.

A rollback is a deploy whose desired state is the recorded release, so it goes through the same plan and rollout as a deploy:

  • every service gets back the exact spec of that release, image digest included;
  • services added since are removed, and services removed since are created again;
  • uploads get back the files of that release, kept on the server by content;
  • unchanged services keep their containers, and routed services switch without downtime.

The images are still on the server as long as the release is kept, so nothing is built, pushed or downloaded. A rollback works even when your registry or your CI is down.

A rollback always moves forward: it creates a new release with the target's version label, shown as active (rollback to 41). To undo a rollback, run dockflow rollback again.

With --service, only that service goes back; the others keep what they run now, and the new release keeps the current version label.

A rollback runs no jobs. It does not replay old migrations, and it does not undo the ones the newer release ran. It does not restore the data in volumes either. If a release changed a database schema, check that the previous version of your app works with it before rolling back.

Interrupted runs

The agent can die in the middle of a run: the server reboots, Docker restarts, its container is killed or runs out of memory. Its release then stays applying, and the kernel releases the lock. Nothing is left locked.

What happens next depends on one write, the commit. The agent records the release as live in env.json once everything else is done.

  • Before the commit, the run is rolled back. Its release ends abandoned, and the previous release serves again.
  • After the commit, the release is live and stays so. It is marked active, the previous one superseded, and the old containers are removed.

The watchdog does this within 10 seconds of the agent's container stopping. That matters for stop-first services: the dead run had stopped the old container on purpose, so Docker does not start it again on its own.

Without the watchdog (server init --no-watchdog), the next run that takes the lock (a deploy, a rollback, a restart…) does it first. Either way:

  1. the containers and jobs the dead run had created are removed;
  2. the containers of the current release get their names back, and those the dead run had stopped start again;
  3. the switch points back at them, and the current release's uploads are restored; a routed service's new containers are removed only after that, so it keeps serving;
  4. the unfinished release is marked abandoned;
  5. then the next run plans from what is actually running, as usual.

A plan always starts from the actual state, so the next deploy brings the app to the desired state whatever the dead run had done.

Losing the CLI or the CI runner interrupts nothing: the agent finishes the run on its own.

Seeing the history

dockflow releases production
SEQ  VERSION  STATUS                  CREATED               BY
43   1.8.1    active                  2026-10-26T14:20:05Z  alice  ← current
42   1.8.0    rolled_back             2026-10-26T14:11:47Z  alice
41   1.7.2    superseded              2026-10-25T09:02:13Z  ci-bot
OptionEffect
-n, --count <n>Show the n most recent releases
--statsAdd durations and the success rate

dockflow status production shows the current release, its services and containers, and the degraded flag. With --json, both print one JSON document.

Edit on GitHub

On this page