Complete Guide to Backing Up Your Docker Homelab
Learn how to automate logical dumps, raw volume copies, compression, encryption, retention, and restore tests for a reliable Docker homelab backup.
Before you start: versions, package names and storage identifiers change over time. Check the commands against the official documentation for your setup before running them, especially anything that creates, deletes or overwrites data.
A Docker homelab backup has to cover three different things, and the method differs for each. Treating them all the same is how people end up with a backup directory full of archives that do not restore.
| What | How to back it up |
|---|---|
| Databases (MariaDB, MySQL, PostgreSQL) | A logical dump from inside the container |
| Named volumes | A helper container with --volumes-from |
| Bind mounts | Copy the host directory |
Plus the Compose files themselves, which are the cheapest thing to back up and the most annoying to reconstruct.
Step 1: Find out what you actually have
Before writing any script, list what is running and what it is storing:
docker ps --format 'table {{.Names}}\t{{.Image}}'
docker volume ls
To see exactly what a given container mounts:
docker inspect -f '{{range .Mounts}}{{.Type}} {{.Source}} -> {{.Destination}}{{"\n"}}{{end}}' <container>
That prints one line per mount with its type, which tells you which of the three methods applies. Anonymous volumes with long hex names show up here too — those are data someone forgot to name, and they are the ones that get lost in a migration.
Step 2: Dump databases, do not tar them
Never back up a running database by archiving its data directory. You will capture files mid-write and get an archive that restores into a corrupt database, usually discovered at the worst moment. Use the database’s own dump tool.
MariaDB and MySQL — a naming change that breaks old scripts
docker exec <db-container> sh -c \
'exec mariadb-dump -uroot -p"$MARIADB_ROOT_PASSWORD" --single-transaction --all-databases' \
> backup.sql
If you are copying an older guide, note the tool name. From MariaDB 11.0 the mysqldump symlink is deprecated and removed from the official MariaDB Docker image. On a mariadb:11 container the old command fails with:
sh: 1: exec: mysqldump: not found
Use mariadb-dump. The MySQL image still uses mysqldump, so check which image you are actually running rather than assuming.
Two details in that command earn their place:
--single-transactiontakes a consistent snapshot on InnoDB without locking the tables, so the service keeps working during the dump.- Reading the password from the container’s own environment variable keeps it out of your shell history and out of the process list.
PostgreSQL
docker exec <db-container> pg_dumpall -U postgres > backup.sql
Check the dump is not empty before trusting it — a failed dump still creates a file, and a zero-byte .sql looks like a backup until you need it:
ls -lh backup.sql && tail -2 backup.sql
A complete MariaDB dump ends with a -- Dump completed line.
Step 3: Back up named volumes
Named volumes live under /var/lib/docker/volumes/, but the documented way to read them is a throwaway container that mounts the same volumes:
docker run --rm --volumes-from <container> \
-v "$(pwd)":/backup ubuntu \
tar cvf /backup/backup.tar /path/in/container
Restoring reverses it:
docker run --rm --volumes-from <container> \
-v "$(pwd)":/backup ubuntu \
bash -c "cd /path/in/container && tar xvf /backup/backup.tar --strip 1"
For anything that is not a database, stop the container first. A volume copied while an application is writing to it has the same consistency problem as a tarred database, just with lower odds of being noticed:
docker compose stop <service>
# take the backup
docker compose start <service>
Step 4: Back up the Compose files
tar czf compose-backup.tar.gz /opt/*/compose.yaml /opt/*/docker-compose.yml
Your .env files live alongside these and contain passwords. Either include them and encrypt the archive, or exclude them and store the secrets in a password manager — but decide, rather than leaving credentials in a plaintext backup on the same host.
Step 5: Retention
find /srv/docker-backups -name '*.tar.gz' -mtime +30 -delete
find /srv/docker-backups -name '*.sql' -mtime +30 -delete
find -mtime is safer than sorting a directory listing and deleting the tail: it acts on file age rather than on position in a list, so an unexpected filename or a missing file cannot shift what gets removed.
Run the deletion after a successful backup, never before. If tonight’s backup fails and the pruning already ran, you have quietly reduced your recovery window.
Step 6: Test a restore — this is the actual backup
An untested backup is a hypothesis. Verify at two levels.
Cheap check, safe to automate nightly:
tar -tzf backup.tar.gz > /dev/null && echo "archive readable"
Real check, worth doing quarterly — restore the database dump into a scratch container and query it:
docker run -d --name restore-test \
-e MARIADB_ROOT_PASSWORD=temp mariadb:11
docker exec -i restore-test sh -c \
'exec mariadb -uroot -ptemp' < backup.sql
docker exec restore-test sh -c \
'exec mariadb -uroot -ptemp -e "SHOW DATABASES; SELECT COUNT(*) FROM yourdb.yourtable;"'
docker rm -f restore-test
A row count you recognise is proof. “The file exists and is about the right size” is not.
A note on off-host copies
Everything above writes to the same machine running the containers. That protects against a bad upgrade or a deleted volume, and not at all against a failed disk or a wiped host. Sync the backup directory somewhere else — another machine, external storage, or object storage — before you consider this finished.
Sources
- Docker docs — Volumes (documented backup and restore procedure with
--volumes-from) - MariaDB docs — mariadb-dump (
mysqldumpsymlink deprecated and removed from the official Docker image in 11.0) - PostgreSQL docs — pg_dumpall
Related reading
Some links on this page may be affiliate links. If you buy through them we may earn a commission at no extra cost to you. See our affiliate disclosure.