How to Back Up and Restore Docker Volumes (and Move a Container to Another Server)

Containers are disposable; the data in their volumes is not. If you've followed this series your apps keep data in ./data folders next to their compose files, which makes backup almost trivial — but named volumes, running databases and the question "how do I move all this to a new box?" each have a wrinkle worth knowing.
Quick reference
| Task | What |
|---|---|
| Bind mount (./data:/app/data) | it's a normal folder — back it up like any other |
| Named volume (appdata:/app/data) | lives in /var/lib/docker/volumes//_data |
| List volumes | docker volume ls |
| Consistent backup of a volume | stop the container, or use the tar-through-container trick |
| Database | dump it (mysqldump, pg_dump) — never copy its files while running |
| Move an app | copy compose folder + data to the new server, docker compose up -d |
1. Know which kind you have
docker inspect nextcloud --format '{{json .Mounts}}' | python3 -m json.tool
"Type": "bind" with a Source path under your home folder is a bind mount — back up the folder. "Type": "volume" with a Name is a named volume stored under /var/lib/docker/volumes/.
2. Back up bind mounts
Your whole ~/docker folder, with restic or rsync (tutorials 23 and 55). For strict consistency, stop the stack first:
cd ~/docker/nextcloud && docker compose stop
rsync -a ~/docker/ /mnt/backup/docker/
docker compose start
A few seconds of downtime at 2 am is a price worth paying for a backup you can trust. For apps that must stay up, back up live and accept that a file mid-write might be inconsistent — fine for most things, not for databases.
3. Back up a named volume
Either back up /var/lib/docker/volumes//_data as root, or use a throwaway container to tar it — the portable way:
docker run --rm -v appdata:/data -v /mnt/backup:/backup alpine \
tar -czf /backup/appdata-$(date +%F).tar.gz -C /data .
Restore into a (new, empty) volume the same way:
docker volume create appdata
docker run --rm -v appdata:/data -v /mnt/backup:/backup alpine \
tar -xzf /backup/appdata-2026-10-08.tar.gz -C /data
4. Databases: dump, don't copy
Copying a database's files while it runs gives you a backup that may not open. Dump instead:
docker compose exec db mariadb-dump -uroot -p"$DB_ROOT_PASSWORD" --all-databases | gzip > /mnt/backup/nextcloud-db-$(date +%F).sql.gz
docker compose exec db pg_dumpall -U gitea | gzip > /mnt/backup/gitea-db-$(date +%F).sql.gz
Restore:
zcat nextcloud-db-2026-10-08.sql.gz | docker compose exec -T db mariadb -uroot -p"$DB_ROOT_PASSWORD"
Put the dump line in the nightly script before the file backup runs.
5. Move an app to another server
- On the old server: docker compose stop in the app's folder.
- Copy the folder (compose file, .env, ./data, dumps) to the new one: rsync -a ~/docker/nextcloud/ john@newserver:~/docker/nextcloud/.
- Named volumes: tar and restore as in step 3.
- On the new server: docker compose up -d. Same images, same config, same data.
- Check ownership — if your UID differs between servers, sudo chown -R the data to the new UID (id).
That's the whole migration. It's the reason to prefer bind mounts in ./data: the app is its folder.
6. Restore onto a fresh server from backup
Install Docker (tutorial 24), restore ~/docker from restic or the backup drive, restore any database dumps, docker compose up -d in each folder. Order rarely matters; each app brings its own database.
Troubleshooting
Restored app starts but data is empty. Paths in the compose file don't match where the data landed, or a named volume was recreated empty. docker inspect the mounts.
"permission denied" after restore. Owner UID changed. ls -ln ./data shows the numeric owner; chown -R to match the container's user.
Database container refuses to start on restored files. You copied live database files. Delete the data folder, start fresh, restore the dump.
Backup of /var/lib/docker is huge. You're including image layers. Back up volumes and compose folders, not the whole directory — images re-download.
Wrapping up
Bind mounts in ./data are folders; named volumes need the tar trick; databases need a dump. Stop the stack for a clean copy if you can. Keep each app's compose file, .env and data together, and moving it to a new server is one rsync and one up -d.
