How to Back Up and Restore Docker Volumes the Right Way
Back up Docker named volumes and bind mounts safely, dump databases consistently, automate it with cron, copy backups off-site and test restores.
· 6 min read
Contents
- Know where your data lives
- Rule 1: Databases need dumps, not file copies
- PostgreSQL
- MySQL / MariaDB
- SQLite
- Rule 2: Back up named volumes with a throwaway container
- Rule 3: Bind mounts are plain folders
- Putting it together: a backup script
- Schedule it
- Rule 4: Keep a copy somewhere else
- Rule 5: Test your restore
- Restore PostgreSQL
- Restore a named volume
- Restore a bind mount
- Checklist
- FAQ
- Does docker compose down delete my volumes?
- Can I just back up /var/lib/docker?
- How often should I back up?
- Further reading
Containers are disposable; your data is not. Everything that matters in a self-hosted setup — databases, uploads, app settings — lives in volumes, and that is what you need to back up. The images can always be downloaded again.
This guide shows how to back up named volumes and bind mounts, how to handle databases so backups are consistent, how to automate everything and — most importantly — how to restore.
Know where your data lives
There are two kinds of persistent storage in Docker:
services:
app:
volumes:
- ./uploads:/app/uploads # bind mount: a folder you choose on the host
- app-data:/app/data # named volume: managed by Docker
volumes:
app-data:
List named volumes and see where Docker keeps them:
docker volume ls
docker volume inspect myproject_app-data
Compose prefixes volume names with the project name (usually the folder name), so app-data becomes myproject_app-data.
Bind mounts are just folders, so any file backup tool can handle them. Named volumes live under Docker's data directory (typically /var/lib/docker/volumes/); the portable way to back them up is through a temporary container, shown below.
Rule 1: Databases need dumps, not file copies
Copying a running database's files can produce a corrupted backup, because the database may be halfway through writing. Either stop the container first, or — better — use the database's own dump tool, which produces a consistent snapshot while it keeps running.
PostgreSQL
docker compose exec -T db pg_dump -U app -d app -F c > backups/app-$(date +%F).dump
-F c creates a compressed custom-format dump that pg_restore can restore selectively. -T disables the pseudo-terminal so binary output is not mangled.
MySQL / MariaDB
docker compose exec -T db sh -c 'mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" --single-transaction --all-databases' > backups/mysql-$(date +%F).sql
On recent MariaDB images, the client binaries are named mariadb-dump and mariadb.
SQLite
docker compose exec app sqlite3 /app/data/app.db ".backup '/app/data/backup.db'"
The .backup command is safe while the app is running. It requires the sqlite3 CLI inside the container; if it is not there, stop the app briefly and copy the file instead. See SQLite vs PostgreSQL for more on SQLite in production.
Rule 2: Back up named volumes with a throwaway container
For volumes holding regular files (uploads, configs, app state), mount the volume read-only into a tiny container and archive it:
docker run --rm \
-v myproject_app-data:/data:ro \
-v "$(pwd)/backups":/backup \
alpine tar czf /backup/app-data-$(date +%F).tar.gz -C /data .
What happens:
-v myproject_app-data:/data:romounts the volume read-only at/data.-v "$(pwd)/backups":/backupmounts a host folder where the archive will land.tar czf … -C /data .archives the volume's contents.--rmdeletes the helper container when done.
For apps that write constantly, stop them for a few seconds to get a consistent copy:
docker compose stop app
# run the docker run … tar command above
docker compose start app
Rule 3: Bind mounts are plain folders
tar czf backups/uploads-$(date +%F).tar.gz -C ./uploads .
Or sync them incrementally to another machine:
rsync -a --delete ./uploads/ backup-host:/backups/myproject/uploads/
Putting it together: a backup script
#!/usr/bin/env bash
# backup.sh — run from the folder containing compose.yaml
set -euo pipefail
stamp=$(date +%F_%H%M)
dest="$HOME/backups/myproject"
mkdir -p "$dest"
echo "Dumping database…"
docker compose exec -T db pg_dump -U app -d app -F c > "$dest/db-$stamp.dump"
echo "Archiving app-data volume…"
docker run --rm \
-v myproject_app-data:/data:ro \
-v "$dest":/backup \
alpine tar czf "/backup/app-data-$stamp.tar.gz" -C /data .
echo "Archiving uploads…"
tar czf "$dest/uploads-$stamp.tar.gz" -C ./uploads .
echo "Removing backups older than 14 days…"
find "$dest" -type f -mtime +14 -delete
echo "Done: $(du -sh "$dest" | cut -f1) in $dest"
New to shell scripts? See Bash scripting basics for what set -euo pipefail does.
Schedule it
crontab -e
15 3 * * * cd /home/deploy/myproject && ./backup.sh >> /var/log/myproject-backup.log 2>&1
That runs every night at 03:15. Check the timing with our cron expression explainer.
Rule 4: Keep a copy somewhere else
A backup on the same disk does not survive a disk failure, a deleted server or ransomware. Follow the 3-2-1 rule: three copies of your data, on two different types of storage, with one copy off-site.
Good off-site options:
- rsync or rclone to another server or object storage (S3-compatible buckets are cheap):
rclone copy ~/backups/myproject remote:myproject-backups
- restic or BorgBackup, which add deduplication, encryption and retention policies — ideal for nightly backups because each run only stores what changed:
restic -r sftp:backup-host:/srv/restic/myproject backup ~/backups/myproject
restic -r sftp:backup-host:/srv/restic/myproject forget --keep-daily 7 --keep-weekly 4 --prune
Encrypt anything that leaves your server. restic and Borg encrypt by default.
Rule 5: Test your restore
A backup you have never restored is a guess. Practice on a test machine or a separate Compose project.
Restore PostgreSQL
docker compose up -d db
docker compose exec -T db pg_restore -U app -d app --clean --if-exists < backups/db-2026-10-28_0315.dump
Restore a named volume
docker volume create myproject_app-data
docker run --rm \
-v myproject_app-data:/data \
-v "$(pwd)/backups":/backup:ro \
alpine sh -c "cd /data && tar xzf /backup/app-data-2026-10-28_0315.tar.gz"
Restore a bind mount
mkdir -p uploads && tar xzf backups/uploads-2026-10-28_0315.tar.gz -C uploads
Then start the stack with docker compose up -d and check that the app shows your data. Write down the steps — under pressure, you will be glad you did.
Checklist
- ☐ Every service's persistent data is in a named volume or bind mount
- ☐ Databases are backed up with their dump tools
- ☐ File volumes are archived (stopping the app when needed)
- ☐ Backups run automatically and log their output
- ☐ Old backups are pruned
- ☐ An encrypted copy is stored off-site
- ☐ A restore has been tested and documented
FAQ
Does docker compose down delete my volumes?
No. down removes containers and networks, but keeps volumes. docker compose down -v deletes named volumes — never run it unless you mean to wipe the data.
Can I just back up /var/lib/docker?
It works only with Docker stopped, and it is not portable between Docker versions or storage drivers. Backing up each volume and database separately is safer and easier to restore.
How often should I back up?
As often as the amount of data you can afford to lose. For most self-hosted apps, nightly is a good default; databases with frequent changes may need more.
Further reading
Keep reading
Self-Hosting on a Raspberry Pi: What Works and How to Set It Up
What a Raspberry Pi can realistically self-host, the hardware that makes it reliable, a step-by-step Docker setup and how to reach it securely from anywhere.
Docker Compose for Beginners: Self-Host Your First App
Learn Docker Compose step by step: install it, write your first compose.yaml, persist data with volumes, connect services and keep everything updated.
How to Secure a New Linux VPS: 10 Steps for the First Hour
A practical hardening checklist for a fresh Ubuntu or Debian server: a sudo user, SSH keys, firewall, automatic updates, fail2ban and backups.