Independent tech intelligence, checked against primary sources.

TechPulseMind Useful technology.
No manufactured hype.

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.

Complete Guide to Backing Up Your Docker Homelab

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-transaction takes 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

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.