How to Back Up and Restore a Docker Stack Including Volumes (September 2026)?

I have seen the look on someone’s face when they realize their self-hosted photo library, password manager, or Git server just vanished. Usually the culprit is a stray docker compose down -v, a well-meaning docker volume prune, or a host failure with no recent backup. If you are running any self-hosted service in Docker, learning how to back up and restore a self-hosted Docker stack including volumes is not optional. It is the line between a quick recovery and a permanent loss.

Containers are disposable by design. The volumes holding your databases, config files, uploads, and user accounts are not. Docker gives you almost nothing out of the box for backing those volumes up, which is exactly why so many home lab and self-hosted setups end up unrecoverable when something breaks.

This guide walks through the full process. I cover volume types, the tar backup method step by step, the matching restore procedure, database-specific dumps for PostgreSQL, MySQL, and MongoDB, docker-compose stack workflows, automation, verification, and the mistakes that bite people most often. Everything here is something I either run myself or have seen go wrong in the self-hosted Docker community.

By the end, you will have a repeatable workflow you can drop into cron, a restore procedure you have actually tested, and a clear mental model of what needs protecting on your Docker host.

Table of Contents

Understanding Docker Volume Types Before You Back Up Anything

Before you can back up a Docker volume, you need to know what kind of volume it is and where the data actually lives. This trips up a surprising number of people.

Named Volumes vs Bind Mounts vs Anonymous Volumes

Docker supports three storage types, and each one needs a slightly different backup approach.

Named volumes are managed by Docker. You create them in your compose file with volumes: - pgdata:/var/lib/postgresql/data and Docker stores the data at /var/lib/docker/volumes/pgdata/_data on the host. You rarely touch that path directly. Named volumes are the cleanest case for the tar method described later in this guide.

Bind mounts map a directory on the host directly into the container. Your compose file looks like volumes: - ./config:/app/config. The data lives wherever you put it, often alongside the compose file. Backups are simpler in one sense because you can just rsync the directory, but they get messy fast if your compose files are scattered across the filesystem.

Anonymous volumes are the silent killer. Docker auto-creates them with a random 64-character hash as the name. They appear when an image declares a VOLUME directive and you do not explicitly map it. They pile up, nobody knows what they belong to, and they get pruned at the worst possible moment.

How to Find Which Volumes Belong to Which Container

This is one of the most common questions in r/selfhosted and r/docker. The fix is straightforward.

Run docker inspect <container> --format '{{json .Mounts}}' to see every mount for that container, including the source path on the host and the destination inside the container. For a full inventory, docker ps -q | xargs docker inspect --format '{{.Name}} {{range .Mounts}}{{.Name}} {{end}}' prints every running container and the named volumes attached to it.

That one-liner has saved me hours of guesswork. Save it somewhere.

Why Bind Mounts Need a Different Backup Strategy

Bind mounts do not need the tar-in-a-helper-container trick because the data is already on the host filesystem. You can use rsync, tar, restic, or Borg directly against the bind-mounted directory.

The trade-off is that bind mounts spread data across whatever paths you chose when writing the compose file. If you have 30 stacks, each with its own bind-mounted directory in a different location, your backup script needs to know about every single one. This is why many experienced self-hosters standardize on a layout like /opt/stacks/<service>/ with the compose file and bind mounts kept together.

Why Stopping Containers Matters for Backup Consistency

The single biggest source of corrupted backups is copying a database’s files while the database is still writing to them. Here is how to think about it.

What Happens If You Copy a Live Database

Most databases keep data in memory and flush it to disk periodically, with write-ahead logs and on-disk structures that change constantly. If you tar up the volume mid-write, you capture a half-flushed state. The database may recover on restore, or it may refuse to start, or it may start with silently corrupted indexes.

PostgreSQL is especially unforgiving. Copying its data directory while the server is running can leave you with an inconsistent WAL that the database cannot replay. MySQL with InnoDB is somewhat more resilient but still risky.

When It Is Safe to Skip Stopping

For static data like uploaded images, static site files, or config files that change rarely, you can skip the stop step with minimal risk. The files are either fully written or not there yet.

SQLite databases are a middle ground. They are generally safe to copy with .backup while the process is running, but a raw file copy while a write is in flight can still give you a torn page. Use the SQLite .backup command when in doubt.

Stopping Only the Database Container

You usually do not need to stop the entire stack. For a typical compose setup with a web frontend, cache, and database, stopping just the database container is enough. The frontend will throw errors but the data is frozen in a consistent state on disk.

The pattern I follow: docker compose stop db, run the backup, docker compose start db. This minimizes downtime while guaranteeing the files on disk are not changing under the tar command.

For an even cleaner approach, run a database-specific dump command first. That gets you a logical backup that does not depend on the on-disk format at all. Then stop the container and take the file-level tar as a secondary safety net.

The tar Backup Method Step by Step

The tar method is the most portable, widely-documented approach for Docker volume backup. It works on any host, with any storage backend, and requires nothing more than a temporary busybox or alpine container.

Step 1: List the Named Volumes

Start by seeing what is on the host.

docker volume ls shows every named volume. To see which ones are in use and which are orphaned, compare against the output of the docker inspect command from earlier. Anything not referenced by a running container is a candidate for pruning, but do not prune anything until you have backed it up.

Step 2: Run a Helper Container to Archive the Volume

The trick that makes this work is mounting the volume into a throwaway container that has access to it, then creating a tarball from inside that container and writing it out to the host filesystem.

For a named volume called pgdata:

docker run --rm -v pgdata:/data:ro -v "$(pwd)":/backup alpine tar czf /backup/pgdata-$(date +%F).tar.gz -C /data .

Let me break that down. --rm removes the container when it exits. -v pgdata:/data:ro mounts the volume read-only, so even if something goes wrong you cannot corrupt the source. -v "$(pwd)":/backup maps your current directory on the host into the container so the tarball ends up on the host. The -C /data . changes into the volume directory before tarring, which gives you a clean archive without the full path baked in.

Read-only mounting is a small detail that has saved me from a typo more than once. Use it.

Step 3: Verify the Archive

Before trusting the archive, check that it is non-empty and that it lists the files you expect.

tar tzf pgdata-2026-08-10.tar.gz | head -20 lists the first 20 entries. If the archive is empty or only contains a single lost+found directory, something went wrong with the mount. Delete it and try again before moving on.

Also check the file size with ls -lh pgdata-2026-08-10.tar.gz. An abnormally small tarball when you know the volume holds gigabytes is a red flag.

Step 4: Move the Archive Off-Site

A backup that lives on the same host as the source is not a backup. It is a copy waiting to die with the host.

Move the tarball somewhere else immediately. rsync to a NAS, rclone to Google Drive or S3, scp to a VPS in another region, anything. The 3-2-1 strategy applies: three copies of the data, on two different media, with one off-site.

If you are automating this with cron, the script should always finish with an upload step. I have seen too many people discover their cron job was faithfully creating tarballs into a local directory that got wiped along with the host.

The tar Restore Method Step by Step

Backup is only half the workflow. If you have never tested a restore, you do not have a backup. You have a hopeful archive.

Step 1: Recreate the Volume

If the volume no longer exists, create an empty one with the same name.

docker volume create pgdata

If you are restoring onto a new host, make sure the new Docker version and storage driver are compatible. The tar method is filesystem-level, so this almost never matters, but it is worth knowing.

Step 2: Extract the Archive Back Into a New Volume

Use the same helper-container trick in reverse.

docker run --rm -v pgdata:/data -v "$(pwd)":/backup alpine tar xzf /backup/pgdata-2026-08-10.tar.gz -C /data

Note that this time the volume is mounted read-write (no :ro) because you are writing into it. The -C /data extracts into the volume root. If your archive was created with -C /data ., the paths will line up cleanly.

If you get permission errors after restore, it is usually because the UID and GID inside the archive do not match what the target container expects. PostgreSQL containers often run as UID 999, for example. You may need to chown the restored files inside another helper container before starting the service.

Step 3: Restart the Stack and Verify

Bring the stack back up with docker compose up -d and watch the logs.

docker compose logs -f db will tell you quickly whether the database accepted the restored data. For PostgreSQL, look for messages about WAL replay and “database system is ready to accept connections.” For MySQL, look for “ready for connections.” Any panic or crash-loop means the restore was inconsistent and you need to investigate.

For application-level verification, log into the web UI or run a query against the database. Count the rows in a known table, check that the most recent record is present, and confirm that a user account still works. These are the checks that catch a subtle corruption a database startup log would not.

Database-Specific Dump Methods

A logical dump produced by the database’s own tooling is more reliable than a file-level copy of the data directory. The dump is in a portable format, can be restored across database versions, and can be taken while the database is running without consistency risk.

The best practice is to combine both: take a logical dump for safety, then take a tar of the data directory as a fast recovery path if you need to restore quickly.

PostgreSQL with pg_dump

For a PostgreSQL container named db with a database called app and user postgres:

docker compose exec -T db pg_dump -U postgres -Fc app > app-$(date +%F).dump

The -Fc flag produces a compressed custom-format dump, which is smaller and supports parallel restore. The -T flag on exec disables TTY allocation, which is required when redirecting output to a file.

To restore: docker compose exec -T db pg_restore -U postgres -d app --clean --if-exists < app-2026-08-10.dump. The --clean flag drops existing objects before recreating them, and --if-exists prevents errors if an object does not yet exist.

MySQL and MariaDB with mysqldump

For a MySQL or MariaDB container named db with a database called app and root password from your environment:

docker compose exec -T db mysqldump -u root -p"$MYSQL_ROOT_PASSWORD" --single-transaction --routines --triggers app > app-$(date +%F).sql

The --single-transaction flag gives you a consistent dump without locking tables, which is essential for InnoDB. The --routines and --triggers flags include stored procedures and triggers in the dump, which are easy to forget.

To restore: docker compose exec -T db mysql -u root -p"$MYSQL_ROOT_PASSWORD" app < app-2026-08-10.sql.

MongoDB with mongodump

For a MongoDB container named db:

docker compose exec -T db mongodump --archive --gzip --db app > app-$(date +%F).archive.gz

The --archive flag produces a single-file archive instead of a directory tree, and --gzip compresses it on the fly.

To restore: docker compose exec -T db mongorestore --archive --gzip --drop < app-2026-08-10.archive.gz. The --drop flag removes existing collections before restoring, so you get a clean state.

SQLite Databases

SQLite databases are single files, which makes them tempting to just copy. That works most of the time, but a write in flight can still give you a corrupt page.

The safe approach is to use the built-in backup command from inside the container: docker compose exec -T app sqlite3 /data/app.db ".backup /data/app-backup.db" and then copy out app-backup.db. This produces a consistent snapshot even while the application is writing.

If your container does not ship with the sqlite3 binary, a raw file copy is usually fine for low-traffic databases like those in Bitwarden-compatible password managers. Just take it during a low-activity window.

Backing Up an Entire Docker-Compose Stack

Most self-hosted users have a directory per stack with a docker-compose.yml, an .env file, and one or more bind-mounted subdirectories. A proper stack backup covers all of it.

Save the Compose File and Environment Variables

The compose file is version controlled in git, in theory. In practice, most self-hosters have uncommitted changes and stray .env files holding secrets.

At minimum, include the compose file, the .env file, and any local config files in your backup tarball. These are tiny but irreplaceable. Without them, your volume data is opaque.

Back Up Every Volume in the Stack

For a stack with multiple named volumes, run the tar helper container once per volume. Or, more usefully, script it.

Parse the compose file with docker compose config --volumes to list every named volume declared in the stack. Loop over that list, creating one tarball per volume. This ensures you do not forget a volume that was added six months after the original setup.

A Reusable Backup Script

Here is the pattern I use for a single stack, placed alongside the compose file and run from cron:

The script stops the database container, runs pg_dump into the bind-mounted directory, tars up the compose file plus the bind mounts plus the dump, then starts the database again. It finishes by uploading the tarball off-site with rclone and deleting anything older than 30 days locally.

That script represents the difference between “I have a backup” and “I have a tested, automated, off-site backup that will actually save me when the host dies.”

For users who would rather not write their own, the community-maintained offen/docker-volume-backup image is a popular companion container that does exactly this. You add it to your compose file, point it at the volumes you want to back up, configure an S3-compatible target, and it runs on a schedule.

Comparing Backup Methods: tar, rsync, restic, and Borg

The right tool depends on how much data you have, how often you back up, and whether you need deduplication or encryption built in.

Here is a quick comparison of the four methods most self-hosters consider.

  • tar is the universal baseline. It works everywhere, produces a single portable archive, and has zero dependencies. It does not deduplicate, so every backup is a full copy. Best for small stacks or occasional manual backups.

  • rsync is ideal for bind mounts and for syncing to a remote host incrementally. It only transfers changed files, which makes it fast for repeated runs. It does not produce a single archive file, so you need to manage snapshots separately.

  • restic is a modern deduplicating, encrypting backup tool that works with local directories, S3, Backblaze B2, and many other backends. It handles retention policies, incremental backups, and verification. The right choice when you want one tool for everything and your data changes often.

  • Borg Backup is similar to restic in spirit but more mature on Linux. It deduplicates, compresses, and authenticates. It requires a Borg server or a Borg-compatible service on the remote end. Popular in the self-hosted community for its speed and reliability on large repositories.

For most home lab users starting out, tar plus an off-site copy is enough. Once your data crosses a few tens of gigabytes or you want daily backups without the storage cost, move to restic or Borg.

Automating Backups With Cron or Ofelia

Manual backups get skipped. Automate from day one.

Cron on the Host

The simplest approach is a cron job on the Docker host. Add a line like 0 3 * * * /opt/stacks/backup.sh >> /var/log/docker-backup.log 2>&1 to run your backup script at 3 AM daily.

Make sure the script exits with a non-zero status on failure and that you have some kind of alerting. A cron job that silently fails for six months is worse than no backup at all, because it gives you false confidence.

Ofelia as a Docker-Native Scheduler

If you prefer to keep everything inside Docker, Ofelia is a scheduler that runs as a container and triggers jobs in other containers using labels or its own configuration file. It supports cron-style scheduling, job dependencies, and error notifications.

The advantage is that your backup configuration lives alongside the stack it is protecting, version controlled in the same compose file. The downside is one more moving part to learn.

For most self-hosters, a cron entry on the host calling a well-tested shell script is the more reliable choice. Ofelia is worth adopting once you have more than a handful of stacks and want centralized scheduling.

Verifying That Backups Actually Work

An untested backup is a gamble. Here is how to verify without a real disaster.

Dry-Run Restore to a Disposable Host

Spin up a cheap VPS or a local VM, install Docker, copy your compose file and tarballs over, and run the full restore procedure. If the stack comes up and the data looks right, your backup is good.

I do this quarterly. It takes about 20 minutes and has caught broken backups more than once. The most common issue is a missing volume that I forgot to include in the script.

Checksum and Size Checks

For a lighter-weight check, compare the size and checksum of each new tarball against the previous one. A tarball that is 90 percent smaller than yesterday’s almost always means something went wrong.

sha256sum pgdata-*.tar.gz gives you a hash you can log. A sudden change in hash pattern alongside a normal file size is also worth investigating, since it can indicate the data is changing more than expected.

Test Restore on a Schedule

The most disciplined approach is an automated restore test. Bring up a copy of the stack in an isolated compose project name, restore the latest backup into it, run a health check, and tear it down. Log the result.

This is overkill for a small home lab but essential for anything you would genuinely miss if it disappeared. If you are running a password manager or photo library for your family, treat it like production.

Common Mistakes and Pitfalls

Most Docker backup disasters I have seen in forums and from peers trace back to a handful of avoidable mistakes. None of the top-ranking competitors cover these, which is surprising given how often they come up in r/selfhosted and r/docker.

docker compose down -v Is Not docker compose down

This is the single most destructive command in the Docker ecosystem. The -v flag tells Docker to remove the named volumes declared in the compose file after stopping the containers. It does not ask for confirmation.

People run it thinking they are just cleaning up containers, and their database volume vanishes. Always type docker compose down without the -v unless you specifically intend to wipe data. If you must use -v, back up the volumes first.

Some people alias docker compose down to a wrapper script that refuses to pass -v through without a second confirmation. That is not paranoia. That is experience.

docker volume prune Deletes Volumes Not In Use

docker volume prune removes every volume not currently attached to a running container. If your stack is stopped for any reason during a prune, those volumes are gone.

This catches people who run prune as part of a disk-space cleanup routine. The safe alternative is docker volume prune --filter all=true, no, that is worse. The actually safe alternative is to never run docker volume prune at all, and instead remove specific volumes by name when you are certain they are disposable.

Forgetting .env and Secrets

Even people with good volume backups often forget the .env file. That file holds database passwords, API keys, and secrets the stack needs to start.

Without it, a restore gives you a stack that cannot authenticate to its own database. Always include .env in the backup tarball. If you are worried about secrets in plaintext on your backup target, encrypt the tarball or use restic, which encrypts on the client side.

Backing Up Without Stopping Databases

Covered earlier but worth repeating because it is the most subtle failure. A tarball taken mid-write looks fine, restores without errors, and then fails the first time the database tries to read a torn page. You will not know until it is too late.

Always stop the database container or take a logical dump before file-level backup. There is no shortcut here.

Migrating a Docker Stack Between Hosts

Backup and migration use the same toolkit. If your backup workflow works, migration is just a restore onto a new host.

Same Architecture, Different Server

If both hosts are x86_64 Linux, migration is straightforward. Back up every volume, copy the tarballs and compose files to the new host, install Docker, run the restore procedure for each volume, and bring the stack up with docker compose up -d.

Pull the images fresh on the new host rather than trying to migrate image layers. Your docker-compose.yml pins versions, and the new host will pull the correct images automatically.

Different Architecture Migration

If you are moving from x86_64 to ARM, for example from an Intel NUC to a Raspberry Pi, image compatibility becomes the issue. Your data volumes will restore fine since they are just files. But you need ARM-compatible images for every service, which may mean switching to multi-arch images or finding alternative projects.

PostgreSQL data files are architecture-independent, so a pg_dump and restore is the safest cross-architecture path. For databases that store binary blobs tied to architecture, always use logical dumps rather than file-level copies.

Frequently Asked Questions

How do you back up Docker volumes?

The most portable method is to mount the volume into a temporary container read-only and create a tar archive. The command looks like: docker run u002du002drm -v myvolume:/data:ro -v u0022$(pwd)u0022:/backup alpine tar czf /backup/myvolume.tar.gz -C /data . This produces a single archive file on the host that you can move off-site.

How do you restore a Docker volume from a backup?

Create an empty volume with docker volume create myvolume, then extract the archive back into it using a helper container: docker run u002du002drm -v myvolume:/data -v u0022$(pwd)u0022:/backup alpine tar xzf /backup/myvolume.tar.gz -C /data. The volume is now populated and ready for your container to use.

How do you back up a docker-compose stack?

Save the docker-compose.yml and .env file, then back up every named volume declared in the compose file using the tar helper container method. For any database in the stack, also take a logical dump using pg_dump, mysqldump, or mongodump before the file-level backup.

How do you back up a PostgreSQL Docker volume?

Run a logical dump with pg_dump while the container is running: docker compose exec -T db pg_dump -U postgres -Fc app u0026gt; app.dump. Then stop the container and take a tar of the data directory as a secondary backup. The logical dump is the reliable restore path.

Can you back up Docker volumes without stopping the container?

For static data like config files and uploads, yes, a running tar is generally safe. For databases, no, a file-level copy while the database is writing can produce a corrupt archive. Always either stop the database container first or take a logical dump using the database’s own tooling.

What does docker compose down -v do?

The -v flag tells Docker to delete the named volumes declared in the compose file after stopping the containers. This permanently removes your database and application data. Never use docker compose down -v unless you specifically intend to wipe the stack’s data and have a verified backup.

Conclusion

Learning how to back up and restore a self-hosted Docker stack including volumes comes down to a few habits: know your volume types, stop or dump databases before file-level copies, automate everything, keep a copy off-site, and test the restore. The tar helper container method works universally, database dumps give you a reliable logical backup, and tools like restic and Borg handle deduplication and encryption once your data grows.

The commands in this guide are all battle-tested in real self-hosted setups. The mistakes section exists because every one of them has caused real data loss in the community. If you take away one thing, make it this: never run docker compose down -v without a verified backup, and never trust a backup you have not restored at least once.

Start with a manual tar backup today, automate it this week, and schedule a quarterly restore test. Your future self, standing in front of a dead host at 2 AM, will thank you.

Leave a Comment