Skip to content

Single container, volume-label Docker-native backups. Think Traefik, but for backups.

Dockup runs once per Docker host. Every volume declares its own backup policy with dockup.* labels, so the policy is versioned next to the application that owns the data. Destinations, credentials, schedules and secrets live in Dockup's own configuration, so they are not duplicated across stacks.

yaml
services:
  app:
    image: ghcr.io/example/app
    volumes:
      - appdata:/var/lib/app

  dockup:
    image: gitea.vladfaust.com/vladfaust/dockup:latest
    restart: always
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - dockup-state:/var/lib/dockup
      - ./dockup.yml:/etc/dockup/dockup.yml:ro
      - ./backups:/backups

volumes:
  appdata:
    labels:
      dockup.enable: "true"
      dockup.schedule: daily
      dockup.target: nas
      dockup.keep-daily: "7"
      dockup.keep-monthly: "6"
  dockup-state:
yaml
# dockup.yml
targets:
  nas:
    type: local
    path: /backups

That is the whole setup: the volume is discovered, archived on schedule, uploaded, and pruned to the retention policy. Volumes created after Dockup started are picked up automatically.

Status: early development. Discovery, configuration, label parsing, the planner, retention, run history (dockup status), the instance lock, the health/metrics endpoints, the archive engine (helper containers, zstd/gzip, scheduled and manual backups, retention pruning), local and S3 targets, age encryption, retries, quiescing with hooks, notifications, catch-up, list and restore are implemented and tested. See docs/architecture.md.

Why labels on volumes ​

The upstream docker-volume-backup reads its configuration from environment variables, which means one backup container per schedule, or a central configuration file that duplicates what each compose file already knows. Issue #329 proposed moving configuration onto Docker objects instead.

Dockup takes that idea literally:

  • The volume is the unit of backup. Its labels are immutable after creation, so the policy cannot drift between restarts.
  • The compose file that declares the volume declares its policy, in the same way Traefik routers are declared with traefik.* labels.
  • Secrets never live in labels. A label is visible to anything that can read the Docker socket; destination credentials stay in Dockup's configuration.
  • One agent per host. No sidecar per stack, no cron container per volume.

Labels ​

Named volumes carry a flat policy:

LabelExampleMeaning
dockup.enabletrueOpt the volume in. Nothing is backed up without it.
dockup.scheduledaily or 0 3 * * *Named schedule from the configuration, or an inline cron expression.
dockup.targetnas,offsiteOne or more configured targets. Defaults to defaults.targets.
dockup.filename{source}_{timestamp}Archive base name template.
dockup.compressionzstdnone, gzip or zstd.
dockup.encrypttrueEncrypt with the configured age recipients.
dockup.verifytrueRe-read the archive after upload and compare its checksum.
dockup.keep-last7Keep the newest N archives.
dockup.keep-daily7Keep one archive per day for the newest N days.
dockup.keep-weekly4Keep one archive per ISO week for the newest N weeks.
dockup.keep-monthly12Keep one archive per month for the newest N months.
dockup.max-age30dHard age cap; anything older is pruned.
dockup.excludecache,*.logPaths to skip inside the source.

Bind mounts are declared on the container, grouped by source name:

yaml
services:
  app:
    labels:
      dockup.bind.data: /srv/app/data
      dockup.data.schedule: hourly
      dockup.data.target: nas
      dockup.bind.config: /srv/app/config
      dockup.stop: "true"        # may be stopped while its sources are archived
      dockup.hook: postgres      # named hook set, run inside this container

Full reference: Labels reference.

Configuration ​

yaml
host: nas-01                 # default: container hostname
listen: ":9779"              # health and metrics
state-dir: /var/lib/dockup   # SQLite state, instance lock, spool directory

defaults:
  schedule: daily
  targets: [nas]
  keep-last: 7
  encrypt: true

schedules:
  hourly: "17 * * * *"
  daily: "23 3 * * *"

targets:
  nas:
    type: local
    path: /backups
  s3:
    type: s3
    endpoint: https://s3.example.com
    bucket: dockup
    access-key: ${S3_ACCESS_KEY}
    secret-key-file: /run/secrets/s3-secret

encryption:
  age:
    recipients: [age1...]

hooks:
  postgres:
    pre: ["pg_dump -Fc -f /var/lib/postgresql/dump.pgcustom app"]
    post: ["rm -f /var/lib/postgresql/dump.pgcustom"]

notify:
  webhook: { url: https://example.com/hook }
  healthcheck: { success: https://hc-ping.com/.../ok, fail: https://hc-ping.com/.../fail }
  telegram: { token: ${TELEGRAM_TOKEN}, chat-id: "42" }

Full reference: Configuration reference.

CLI ​

console
$ dockup plan              # what would be backed up, when, and where
$ dockup backup            # run a backup now (all sources, or --source)
$ dockup backup --dry-run  # show the archive keys a backup would create
$ dockup list              # archives stored for a source
$ dockup restore --source volume:appdata --dry-run
$ dockup restore --source volume:appdata --force
$ dockup prune --dry-run   # show what retention would delete
$ dockup status            # last runs, next runs, issues (works offline)
$ dockup status --history 20 --json
$ dockup config validate   # check the configuration file
$ dockup config show       # effective configuration, secrets redacted
$ dockup daemon            # run in the foreground (container default)
$ dockup version

Security ​

Dockup needs the Docker socket to discover volumes, create short-lived archive containers and stop labelled workloads. That is equivalent to root on the host. Mount it read-write and treat the Dockup container accordingly:

  • destination credentials live in the configuration or in mounted secret files;
  • the archive helper runs with a read-only volume mount, no network and a read-only root filesystem;
  • containers are only ever stopped when they carry dockup.stop=true.

Development ​

Requires Go 1.26 and, for the image, Docker with buildx.

console
$ make test        # unit tests
$ make race        # unit tests with the race detector
$ make lint        # gofmt, go vet, staticcheck
$ make docker      # build the image locally

The design, milestones and test strategy are described in Architecture. Contributions are welcome — see Contributing.

License ​

Mozilla Public License 2.0. See LICENSE.

Documentation website ​

The documentation is also published as a site — VitePress, rendered to static files and served by nginx in its own image:

console
$ docker run -d -p 8080:80 gitea.vladfaust.com/vladfaust/dockup-website:latest

The pages are generated from docs/, README.md and CONTRIBUTING.md, so there is one copy of every sentence: it renders on the Gitea page and on the site. The CLI reference is generated from dockup --help, and npm run freeze -- v0.1 snapshots the docs for a release (the navbar gets a version switcher). See website/README.md.

Released under the Mozilla Public License 2.0.