DevPlainly
Self-Hosting

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
  1. Know where your data lives
  2. Rule 1: Databases need dumps, not file copies
  3. PostgreSQL
  4. MySQL / MariaDB
  5. SQLite
  6. Rule 2: Back up named volumes with a throwaway container
  7. Rule 3: Bind mounts are plain folders
  8. Putting it together: a backup script
  9. Schedule it
  10. Rule 4: Keep a copy somewhere else
  11. Rule 5: Test your restore
  12. Restore PostgreSQL
  13. Restore a named volume
  14. Restore a bind mount
  15. Checklist
  16. FAQ
  17. Does docker compose down delete my volumes?
  18. Can I just back up /var/lib/docker?
  19. How often should I back up?
  20. 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:ro mounts the volume read-only at /data.
  • -v "$(pwd)/backups":/backup mounts a host folder where the archive will land.
  • tar czf … -C /data . archives the volume's contents.
  • --rm deletes 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