Dockflow

Backups

Back up databases and volumes with restic, encrypted and deduplicated, on the server or off-site, with retention, and without stopping a game server.

Dockflow backs up the stateful services of an app with restic. Each backup is a snapshot: encrypted, deduplicated (a second backup stores only what changed) and checked.

To back up a service, declare x-dockflow.backup on it, then run dockflow backup run:

compose.yml
services:
  db:
    image: postgres:17
    environment:
      POSTGRES_DB: shop
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes: [pgdata:/var/lib/postgresql/data]
    x-dockflow:
      stateful: true
      backup:
        type: postgres
        keep: { daily: 14, weekly: 8 }
volumes:
  pgdata: {}
dockflow backup run production
dockflow backup list production
SNAPSHOT  TIME                  SERVICE  RELEASE  SIZE
3f1c2a9b  2026-10-27 18:02:11Z  db       12       4.2 MiB

What is backed up

A service is backed up when it is stateful and declares backup. Its type says how:

typeHow it is captured
postgrespg_dump --format=custom, run in the service's own container, so the dump tool is the server's version. The user is POSTGRES_USER (default postgres), the database database or POSTGRES_DB
volumeThe service's volumes and bind mounts, or those listed in volumes, read-only, with its writes stopped as quiesce says
commandThe dump command, run in the service's container: its standard output is the backup. restore reads it back on its standard input
mysqlmysqldump --single-transaction --routines --events (or mariadb-dump) of database, MYSQL_DATABASE or MARIADB_DATABASE, or every database. The credentials are the image's: MYSQL_ROOT_PASSWORD or MARIADB_ROOT_PASSWORD, else MYSQL_USER and MYSQL_PASSWORD
mongomongodump --archive, of database or every database, as MONGO_INITDB_ROOT_USERNAME
redisredis-cli --rdb, with REDIS_PASSWORD when it is set

The options of backup:

OptionTypeDefaultDescription
typestringrequiredSee above.
databasestringthe image'spostgres, mysql, mongo: the database to dump.
dump_options, restore_optionsstringnoneExtra arguments of the dump tool (postgres, mysql, mongo, redis) and of the restore tool (postgres, mysql, mongo; a redis restore replaces the data file and takes none), split on spaces, like "--no-owner --no-acl".
volumeslistevery volume and bind mountvolume: the volumes, by their key in the Compose file, and bind mounts, by their path on the server, to copy.
quiescestop, none or mapstopvolume: how writes stop during the copy. stop stops the service's containers, then starts them again; none copies while it runs; { exec: { pre: [...], post: [...] } } runs pre in the running container before the copy and post after, even when the copy fails or the run is cut: post may then run twice, so it must be harmless to repeat.
dump, restoreargument listsrequired for commandThe commands of the command type.
keepmap{ last: 7, daily: 14, weekly: 8, monthly: 6 }The snapshots kept: last, hourly, daily, weekly, monthly, as restic's forget --keep-*.

To back up a game server without stopping it, pause its saves around the copy with quiesce.exec:

compose.yml
services:
  game:
    image: itzg/minecraft-server
    volumes: [world:/data]
    x-dockflow:
      stateful: true
      backup:
        type: volume
        quiesce:
          exec:
            pre: ["sh", "-c", "rcon-cli save-off && rcon-cli save-all flush"]
            post: ["rcon-cli", "save-on"]
        keep: { hourly: 24, daily: 14 }

Where backups go

Without configuration, backups go to a restic repository on the server, in the dockflow-backups volume.

Its password is generated on the first backup. It is stored in /var/lib/dockflow/backups/local.password, readable by root only, and the first backup tells you so. Keep a copy of that password elsewhere: the backups cannot be read without it.

A repository on the same disk as the data does not survive the loss of the server. To send backups off-site, set backups in dockflow.yml, with the storage's credentials taken from your secrets:

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}
  • repository is any restic repository reachable from the server: s3: (AWS and S3-compatible storage such as OVH, Scaleway or MinIO), b2:, azure:, gs:, sftp:, rest:.
  • env is passed to restic.
  • The first backup creates the repository, and its bucket when the credentials allow it.
  • Without password, Dockflow generates one, as for the local repository, in /var/lib/dockflow/backups/<id>.password.

Where credentials go

The databases' credentials never leave their containers. The dump and restore commands read them from the image's variables, and pass them to the tool in a temporary file, never on a command line.

The repository's password and the storage's credentials reach restic through a file on the server's tmpfs, readable by root and removed at the end of the run. They never appear on a command line, in a container's configuration or on disk: when /run/dockflow is not a tmpfs, the backup fails with secret.not_tmpfs before writing them.

How a backup runs

  1. dockflow backup run [<env>] [<service>] takes the environment's lock. A backup and a deploy never run at the same time (--wait-lock on a deploy waits for the backup).
  2. The agent opens the repository, and creates it on the first backup.
  3. It backs up each service of the current release that declares backup, or the one you named. This is what the server runs, not your working tree.
  4. A dump streams from the service's container into restic, without a temporary file. restic saves it only once the dump has succeeded and all of it has arrived: a dump that fails, or a run that is cut, leaves no snapshot.
  5. Each snapshot is tagged with the app, the environment, the service and the release. Its host is the server's name.
  6. The service's retention is applied (restic forget). Once a day, the space of forgotten snapshots is reclaimed (--prune).
  7. restic check reads 1% of the repository's data, chosen at random, to catch corruption early.

When a service's backup fails, the other services are still backed up, and the command exits with code 12.

backup run needs only the connection string and the repository's secrets. It reads dockflow.yml, not the Compose files.

Scheduling

Nothing runs on a schedule on the server. Run dockflow backup run on a schedule from your CI, or from any cron that has the connection string.

The GitHub workflow that dockflow init --ci github writes runs it every night. The GitLab pipeline that --ci gitlab writes runs it when a pipeline schedule triggers it. To add it to an existing workflow:

.github/workflows/backup.yml
on:
  schedule: [{ cron: "17 3 * * *" }]
jobs:
  backup:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: curl -fsSL https://raw.githubusercontent.com/Shawiizz/dockflow/main/install.sh | sh
      - run: dockflow backup run production && dockflow backup run production --server
        env:
          DOCKFLOW_SECRETS: ${{ toJSON(secrets) }}

Restoring

dockflow backup restore production db                          # the latest snapshot
dockflow backup restore production db --snapshot 3f1c2a9b      # one snapshot, by its id
dockflow backup restore production db --snapshot 2026-10-20    # the newest one taken by that day
dockflow backup restore staging db --from production           # production's data into staging

backup restore restores a snapshot into the service of the current release, under the environment's lock. It asks for confirmation on a terminal; without a terminal, it needs --yes.

How each kind of backup is restored:

  • A dump streams from the snapshot into the running service, with options that stop at the first error:

    • Postgres: pg_restore --clean --if-exists --single-transaction --exit-on-error, which also rolls back what it did;
    • MySQL: the mysql (or mariadb) client;
    • MongoDB: mongorestore --archive --drop --stopOnError;
    • the command type: its restore command.

    restore_options adds arguments. MySQL and MongoDB change the data as they read the dump, so the dump is first written whole to a temporary file in the container, and its size checked, before the client reads it: a dump cut short changes nothing. A restore that the database itself stops half-way (an SQL error) is not rolled back, except by Postgres.

  • Redis loads its data when it starts. The dump is written beside its data while it runs; then the service is stopped, the dump replaces /data/dump.rdb in its container (the official image's data directory) in one step, and the service starts again. With appendonly yes, Redis loads its append-only file instead: turn it off, or back Redis up as a volume.

  • Volumes are restored into new staging volumes first, dockflow-restore-<run>-<n>, while the service runs. Then the service is stopped, each volume's files are replaced with its staging volume's, and the service starts again. Each volume gets the snapshot's files, and loses those the snapshot does not have. The server needs free space for a copy of the snapshot during the restore; the staging volumes are removed at the end.

--from <env> restores a snapshot of another environment of the same app. It reads that environment's repository, with that environment's secrets: the repository on the server when both run on the same server, or its off-site backups.repository.

A restore that fails exits with code 12, and says why.

When a run is cut

A backup or a restore can be cut at any point: the connection drops, the agent is killed, the server reboots. Dockflow records what it changes before it changes it, in /var/lib/dockflow/apps/<app>-<env>/backup.json, and the watchdog, within seconds, or the next deploy, backup run or backup restore, finishes it:

  • a service stopped or quiesced for a backup or a restore starts again, or gets its post command;
  • a volume restore cut before its copy began leaves the volumes as they were; one cut during the copy is copied again from the staging volumes before the service starts, so the service never runs on a volume half-restored;
  • a backup cut short leaves no snapshot; backup list and backup restore only see complete ones.

If the copy of a volume restore cannot finish (a full disk), the service stays stopped rather than start on a part of the snapshot, and the watchdog tries again: free the space, or run backup restore again.

Retention and pruning

After each backup, the service's keep is applied: restic forgets the snapshots it does not keep. Once a day, it reclaims their space.

backup prune does it now, with each service's retention or the one you give:

dockflow backup prune production                   # each service's keep
dockflow backup prune production db --keep-last 3  # db keeps its 3 most recent snapshots

Options: --keep-last, --keep-hourly, --keep-daily, --keep-weekly, --keep-monthly.

The server's state

/var/lib/dockflow holds the server's state: every release record (the history that rollback and releases read), the uploads, the routes and the server's settings. --server backs it up to the environment's repository:

dockflow backup run production --server     # /var/lib/dockflow, to production's repository
dockflow backup list production --server
dockflow backup prune production --server --keep-last 7
  • The snapshots are tagged with the server's name, separately from the services' snapshots.
  • The default retention applies.
  • The passwords in /var/lib/dockflow/backups/ are left out: a snapshot that holds its own repository's password protects nothing.

These backups exist to rebuild a lost server, so send them off-site.

To rebuild, run dockflow server init on the new server, then put the state back with restic:

restic -r <repository> restore latest --tag server=<name> --target /

The next dockflow deploy of each app then runs on its history.

Without Dockflow

The repository is a standard restic repository. With its password, restic reads it on its own:

  • restic -r <repository> snapshots lists the snapshots;
  • restic restore <id> --target <dir> restores one;
  • restic dump <id> /db.pgdump > db.pgdump extracts a dump.

The repository on the server is in the dockflow-backups volume.

Edit on GitHub

On this page