Docker Compose Backup Strategies for Beginners: What to Copy and How to Prove It Restores
Ellis Crowe
September 27, 2026
The most common Docker Compose backup I see from people starting out is a copy of the docker-compose.yml file and nothing else. It feels like a backup. It is the recipe. But the recipe does not contain the meal: the photos in Immich, the passwords in Vaultwarden, the history in your wiki, the dashboards you spent a weekend building. Those live in volumes and databases, and they are what you actually lose when the SSD dies.
The second most common setup is a nightly job that tars up the whole Docker directory while every container keeps running. That one usually works, until the day you restore it and a Postgres database refuses to start because it was copied mid-write.
This guide is the version I wish I had when I moved my first stack onto a small home server. It is not the only way to do it, but it is simple, it handles the traps that bite beginners, and it ends with the step almost everyone skips: proving the backup restores.
Step one: know what you are actually backing up
A running Compose stack has four kinds of stuff. They need different treatment.
- Configuration you wrote. The
docker-compose.ymlfiles,.envfiles, reverse proxy configs, and any small config files you mount into containers. Small, precious, and easy to lose track of. - Application data in bind mounts. Folders on the host that you mapped into containers, like
./data:/app/data. You can see them with a file manager. Straightforward to back up, as long as nothing is writing to them mid-copy. - Application data in named volumes. Volumes declared in the Compose file and managed by Docker, usually stored under
/var/lib/docker/volumes/. Invisible unless you go looking, which is exactly why they get forgotten. - Databases. Postgres, MariaDB, MySQL, and SQLite files inside either of the above. These deserve special handling because copying their files while they run can produce a backup that looks complete and is not.
What you do not need to back up: the container images themselves. You can pull them again. Do note which image tags you use, though. If your Compose file says latest, a restore six months from now might pull a different major version than the one that wrote your data. Pinning image versions is a backup decision as much as an upgrade one.
Step two: organize your stacks so backups are boring
The easiest stacks to back up follow one convention: every app gets its own folder, and everything that app needs lives inside that folder.
/srv/stacks/
vaultwarden/
docker-compose.yml
.env
data/
immich/
docker-compose.yml
.env
library/
postgres/
paperless/
...
With this layout, backing up /srv/stacks captures nearly everything. Beginners often start with named volumes because the documentation examples use them. There is nothing wrong with named volumes, but converting to bind mounts inside each stack’s folder makes backups dramatically simpler, because you can see and copy the data like any other directory.
If you keep named volumes, write down which ones matter. docker volume ls lists them, and docker volume inspect name shows where they live on disk. Anonymous volumes with long random names are a common place for data to hide; if a container created one, find out what is in it before you assume it is disposable.
Put your Compose files and non-secret config in a Git repository. It costs nothing and gives you history. Keep .env files with passwords out of any public repo; back them up with the rest of your data, encrypted.

Step three: handle databases properly
This is where most beginner backups quietly fail. A database keeps some of its state in memory and writes files in a particular order. If you copy the files while it is running, you may capture a half-written state. Sometimes the database recovers on restore. Sometimes it does not, and you find out at the worst possible time.
There are two safe approaches.
Option A: dump the database while it runs
Every major database has a tool that exports a consistent snapshot while it keeps serving requests. You run it inside the container and save the output to a file that your regular backup picks up.
For Postgres:
docker compose exec -T db pg_dump -U myuser mydb > backups/mydb.sql
For MariaDB or MySQL:
docker compose exec -T db mariadb-dump -u root -p"$DB_ROOT_PASSWORD" --single-transaction mydb > backups/mydb.sql
For SQLite, which many small apps use, the sqlite3 command’s .backup command produces a safe copy, if the tool exists in the container or on the host:
sqlite3 data/app.db ".backup 'backups/app.db'"
The -T flag on docker compose exec matters when you run this from a script or cron: it disables the pseudo-terminal that would otherwise garble the output. Forgetting it is a classic “works when I type it, produces an empty file at 3 a.m.” bug.
Dumps are portable and easy to restore into a newer database version, which is a big advantage if you ever migrate. The downside is that you are backing up the dump, not the live data files, so the backup is only as fresh as the last dump.
Option B: stop the stack, copy, start it again
The bluntest approach is also the most reliable: stop the containers, copy the files, and start them back up.
cd /srv/stacks/immich
docker compose stop
# run your backup of this folder here
docker compose start
With everything stopped, nothing is writing, so a file-level copy is consistent. For a home server, a few minutes of downtime at 4 a.m. is usually a non-issue. The main risk is a script that stops the stack, fails during the backup, and never restarts it. Make sure your script starts the containers again even when the backup step errors out, and have something tell you when the job did not finish.
I use dumps for the big, important databases and stop-and-copy for small apps where the downtime is invisible. Either is far better than copying live database files and hoping.
Step four: pick a backup tool that does the heavy lifting
You could back up with tar and rsync, and plenty of people do. But a proper backup tool gives you three things that matter a lot once you have more than a few gigabytes: deduplication, so nightly backups do not store the same files again; encryption, so an off-site copy is safe on someone else’s disk; and snapshots, so you can restore last Tuesday instead of only last night.
The usual choices are restic, Borg, and Kopia. All three are open source, deduplicate, encrypt, and keep multiple snapshots. restic and Kopia talk directly to cloud object storage like Backblaze B2 or S3-compatible services; Borg works best over SSH to another machine or a Borg-friendly host. If you are torn between the first two, the useful difference shows up at restore time rather than in the feature list, which is what restic versus Borg on a real 4 TB restore digs into.
There is also a container-native option: projects like offen/docker-volume-backup run as a sidecar in your Compose file, can stop labeled containers before a backup, archive named volumes, and push the result to local storage or S3-compatible targets. They are convenient if you want everything declared in YAML, though they add one more container you need to trust.
Step five: a simple nightly script
Here is the shape of the script I recommend to people starting out. It dumps databases, then backs up the whole stacks directory with restic, then prunes old snapshots. Adjust names and paths to your setup.
#!/usr/bin/env bash
set -euo pipefail
export RESTIC_REPOSITORY="/mnt/backup/restic"
export RESTIC_PASSWORD_FILE="/root/.restic-pass"
mkdir -p /srv/stacks/immich/backups
cd /srv/stacks/immich
docker compose exec -T database pg_dump -U postgres immich
> backups/immich.sql
restic backup /srv/stacks
--exclude "/srv/stacks/immich/postgres"
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
Notice the exclude: because the Postgres dump is included, the live Postgres data directory is skipped. That keeps the backup smaller and avoids storing an inconsistent copy that might tempt you during a restore.
Run it with cron or a systemd timer. Then add one line at the end that pings a heartbeat service, whether hosted or self-hosted, so you get an alert when the job does not report in. A backup script that silently stopped running in March is the most common failure I have seen, and no amount of clever scripting catches it without an outside check.

Step six: get a copy out of the house
A backup on a USB drive plugged into the same server protects you from deleting the wrong folder. It does not protect you from a power surge, a burst pipe, theft, or ransomware that can see every mounted disk.
The classic rule is 3-2-1: three copies of your data, on two different kinds of storage, with one off-site. For a home server, a realistic version looks like this:
- The live data on the server.
- A local restic or Borg repository on a second disk or a NAS, for fast restores.
- An encrypted off-site copy in cloud object storage or on a friend’s machine, for disasters.
restic and Kopia can back up to a second repository with the same command pointed at a different destination, or copy snapshots between repositories. Encryption is built in, so the cloud provider only ever sees encrypted chunks. Write your repository password down somewhere that is not the server. A perfectly good encrypted backup with a lost password is indistinguishable from no backup.
Step seven: restore something, on purpose
Everything above is theory until you restore. The first restore test is where you discover that the dump file is empty, that you excluded the wrong folder, that the .env file was never included, or that your restore takes six hours instead of ten minutes.
A good beginner drill:
- Pick one app, ideally a small one.
- On a different machine or a spare VM, install Docker.
- Restore that app’s folder from your backup repository.
- If it uses a database, start only the database container and load the dump:
cat backups/mydb.sql | docker compose exec -T db psql -U myuser mydb. - Start the rest of the stack and log in. Check that recent data is there.
- Time the whole thing and write down every step you had to figure out.
Those notes are the most valuable part of the exercise. On the day your server actually dies, you will be stressed, tired, and probably without the documentation you meant to write. A short restore checklist printed or stored somewhere outside the server turns that day from a crisis into a chore.
Repeat the drill every few months, and after any big change to your stack layout. Backups do not usually fail loudly. They drift: a new app goes into a folder outside the backup path, a database gets upgraded and the dump command changes, a password gets rotated. A restore test is the only thing that catches drift.
A short checklist
- Every stack in its own folder, with data in bind mounts inside that folder where practical.
- Compose files in Git; secrets backed up encrypted, not committed publicly.
- Databases dumped with their own tools, or the stack stopped before copying.
- A deduplicating, encrypting backup tool with retention rules.
- A nightly job that alerts you when it fails to run, not only when it errors.
- One local copy for speed, one off-site copy for disasters.
- A timed restore test, with notes, repeated a few times a year.
None of this is complicated, and the whole setup takes an afternoon. The difference between people who lose data and people who do not is rarely the tool they picked. It is whether they ever tried to get their data back before they needed it.