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.
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:# dockup.yml
targets:
nas:
type: local
path: /backupsThat 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,listandrestoreare 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:
| Label | Example | Meaning |
|---|---|---|
dockup.enable | true | Opt the volume in. Nothing is backed up without it. |
dockup.schedule | daily or 0 3 * * * | Named schedule from the configuration, or an inline cron expression. |
dockup.target | nas,offsite | One or more configured targets. Defaults to defaults.targets. |
dockup.filename | {source}_{timestamp} | Archive base name template. |
dockup.compression | zstd | none, gzip or zstd. |
dockup.encrypt | true | Encrypt with the configured age recipients. |
dockup.verify | true | Re-read the archive after upload and compare its checksum. |
dockup.keep-last | 7 | Keep the newest N archives. |
dockup.keep-daily | 7 | Keep one archive per day for the newest N days. |
dockup.keep-weekly | 4 | Keep one archive per ISO week for the newest N weeks. |
dockup.keep-monthly | 12 | Keep one archive per month for the newest N months. |
dockup.max-age | 30d | Hard age cap; anything older is pruned. |
dockup.exclude | cache,*.log | Paths to skip inside the source. |
Bind mounts are declared on the container, grouped by source name:
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 containerFull reference: Labels reference.
Configuration
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
$ 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 versionSecurity
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.
$ make test # unit tests
$ make race # unit tests with the race detector
$ make lint # gofmt, go vet, staticcheck
$ make docker # build the image locallyThe 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:
$ docker run -d -p 8080:80 gitea.vladfaust.com/vladfaust/dockup-website:latestThe 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.