IAnswerable

Back Up Docker Volumes Properly

29 Sep 2026

A Docker volume backup that you’ve never restored isn’t a backup. The command to take a snapshot is short, but the one that actually matters is the restore — test it before you need it, not during an outage.

The core trick

You can’t just cp a volume’s files from the host, because named volumes live under /var/lib/docker/volumes/ and copying them directly bypasses whatever the container’s storage driver is doing, plus it usually requires root. The reliable way is to run a throwaway container that mounts the volume and tars it.

Backing up

docker run --rm \
  -v mydata:/data \
  -v "$(pwd)":/backup \
  alpine tar czf /backup/mydata-backup.tar.gz -C /data .

This starts a minimal Alpine container, mounts the volume you’re backing up at /data and your current host directory at /backup, tars the volume’s contents, and exits — --rm cleans up the container immediately after.

If the volume backs a database, stop or quiesce the container writing to it first. A backup taken while Postgres or MySQL is mid-write can be internally inconsistent — you’d rather use pg_dump or mysqldump for databases specifically, and reserve the raw volume backup for things like uploaded files or app state that isn’t a database.

Restoring

This is the part people skip until it’s an emergency:

docker volume create mydata-restored

docker run --rm \
  -v mydata-restored:/data \
  -v "$(pwd)":/backup \
  alpine tar xzf /backup/mydata-backup.tar.gz -C /data

Restore into a fresh volume name first, not directly over the original — that way if the archive is corrupt or you extracted the wrong file, you haven’t destroyed the volume you were trying to protect. Once you’ve confirmed it’s good, point your container at the restored volume, or rename volumes to swap them in.

The flow

sequenceDiagram
    participant You
    participant App as "App Container"
    participant Helper as "Alpine Helper"
    participant Archive as "backup.tar.gz"
    You->>App: docker stop (if DB)
    You->>Helper: docker run --rm -v mydata:/data
    Helper->>Archive: tar czf
    You->>App: docker start
    Note over Archive: later, on restore
    You->>Helper: docker run --rm -v mydata-restored:/data
    Helper->>Archive: tar xzf
  

Automating it

A daily cron entry running the backup command, with a date in the filename and a retention cleanup:

#!/bin/bash
DATE=$(date +%F)
docker run --rm -v mydata:/data -v /backups:/backup \
  alpine tar czf /backup/mydata-$DATE.tar.gz -C /data .
find /backups -name "mydata-*.tar.gz" -mtime +14 -delete

That keeps two weeks of daily snapshots and deletes anything older. Point this at off-box storage (rsync to another host, or sync to object storage) — a backup that lives on the same disk as the original doesn’t survive the failure mode you actually care about, which is losing that disk.

What breaks

Permissions. If the original process wrote files as a specific UID inside the container, and you restore with a different UID mapping (common if you moved to rootless Docker, or a different container image), the app may fail to read its own restored files. Check ownership after restore with docker run --rm -v mydata-restored:/data alpine ls -la /data.

Partial backups from a running database. Covered above, but worth repeating: tarring a live database’s data directory without stopping it or using the database’s own dump tool can produce a file that looks fine and fails to restore, or restores in a corrupted state. You won’t find out until you need it.

Silent tar failures. tar can exit 0 even if a file was skipped (permission denied on one file, for instance) — check the archive’s file count against what you expect, or add --exit-code=1 behavior by checking $? explicitly and logging failures somewhere you’ll actually see them.

The one rule that matters

Schedule a restore test, not just a backup schedule. A cron job that’s been silently failing for a month looks identical to one that’s working, until the day you need the file that isn’t there.