Docker Compose Homelab Backups That Actually Restore
Backups are only useful if you can restore them. This guide lays out a practical Docker Compose homelab backup pattern built around state inventory, database dumps, file backups, off-box copies, and restore drills.
Most homelab backup guides start with a tool. I think that is backwards.
For a Docker Compose stack, the tool is the easy part. The hard part is knowing what state matters, stopping yourself from backing up junk, and proving that a restore works before a disk dies or a migration goes sideways.
This is the backup model I would use for a practical home-production Compose host: inventory the state, dump databases in an application-aware way, back up bind mounts and named volumes, copy the result off the box, and schedule a small restore drill. It is not enterprise backup theater. It is the minimum useful SRE loop for services you actually care about.
Who this is for
This is for people running self-hosted apps with Docker Compose on a mini PC, VM, Proxmox guest, or small server. Think apps like Payload CMS, Plex sidecars, dashboards, monitoring, automations, reverse proxies, databases, and media-adjacent services.
Assumptions:
- You use Docker Compose rather than Kubernetes.
- Your important services keep state in bind mounts, named Docker volumes, or databases.
- You can run a scheduled script on the host.
- You want a backup that can survive losing the host, not just a local copy on the same disk.
If you only have stateless containers and every config file already lives in Git, this will feel heavy. If you have family photos, production-ish apps, documents, DNS automation, password stores, or a blog CMS on the box, it is worth being deliberate.
The short version
A useful Compose backup plan has five parts:
- Keep Compose files, environment templates, and runbooks in Git.
- Dump databases with database-native tools before backing up their files.
- Back up application data directories and named volumes with a deduplicating backup tool.
- Copy backups to at least one off-box target.
- Test restores on a disposable machine or VM on a schedule.
The last step is the one most people skip. It is also the step that turns a backup script into an actual recovery plan.
What needs to be backed up
Start with an inventory. For each Compose project, write down the state that matters and how you would recreate it.
| State type | Examples | Best backup approach | Restore risk |
|---|---|---|---|
| Compose definitions | compose.yml, override files, systemd units | Git repository | Low if secrets are handled separately |
| Secrets and env | .env, API keys, app secrets | Password manager, encrypted secret store, or encrypted backup | High if missing or stale |
| Databases | Postgres, MariaDB, Redis persistence | Native dumps plus optional file backup | High if copied while inconsistent |
| App uploads | CMS media, documents, config uploads | File backup or object storage replication | Medium to high |
| Docker named volumes | App data hidden under Docker volume paths | Docker volume tar/export or mount-aware backup | Medium |
| Large replaceable media | Linux ISOs, transcoding cache, downloads | Usually exclude or back up separately | Low unless unique |
| Observability data | Prometheus, logs, uptime history | Usually short retention or selective backup | Low to medium |
The goal is not to back up every byte. The goal is to recover the service to a known-good state with the least surprise.
A simple backup architecture
For a single Compose host, I like this shape:
Compose host
├─ /srv/stacks/<app>/compose.yml
├─ /srv/stacks/<app>/.env
├─ /srv/appdata/<app>/...
├─ Docker named volumes
└─ /var/backups/homelab/staging
├─ postgres-dumps/
├─ volume-exports/
└─ backup-manifest.json
Backup job
├─ creates database dumps
├─ exports named volumes that need it
├─ runs restic or kopia against staging + appdata
└─ copies encrypted snapshots to off-box storage
Restore drill
└─ restores into a disposable VM or test directory and starts the app with a temporary Compose fileRestic and Kopia are both reasonable choices for encrypted, deduplicated backups. Restic is widely used, script-friendly, and supports many backends. Kopia has a polished snapshot model and a UI option. The important point is not which one wins. The important point is that the repository is encrypted, off-box, and periodically checked.
Database dumps first, files second
Databases are where many homelab backups silently fail. Copying a live database directory can work only when the database engine, filesystem, and snapshot method all line up. A plain file copy of a running Postgres or MariaDB data directory is not the backup plan I would trust first.
Use database-native exports as the portable recovery artifact:
- Postgres:
pg_dumpfor one database, orpg_dumpallwhen roles and globals matter. - MariaDB/MySQL:
mariadb-dumpormysqldump, depending on image and version. - SQLite: use the SQLite backup command or stop the app briefly before copying the database file.
- Redis: decide whether Redis is cache or durable state. If it is durable, back up its configured persistence file and understand the recovery tradeoff.
For Postgres in Compose, the pattern usually looks like this:
#!/usr/bin/env bash
set -euo pipefail
APP="payload"
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
BACKUP_ROOT="/var/backups/homelab/staging"
DUMP_DIR="$BACKUP_ROOT/postgres-dumps/$APP"
mkdir -p "$DUMP_DIR"
docker compose -f /srv/stacks/$APP/compose.yml exec -T postgres \
pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" --format=custom \
> "$DUMP_DIR/${APP}-${STAMP}.dump"
sha256sum "$DUMP_DIR/${APP}-${STAMP}.dump" \
> "$DUMP_DIR/${APP}-${STAMP}.dump.sha256"A few notes:
exec -Tavoids pseudo-TTY issues in cron.--format=customgives you a compressed archive that works well withpg_restore.- Keep the matching restore command in the same repo as the backup job.
- Do not assume the app container environment has the right database credentials unless you have verified it.
Named volumes need special handling
Bind mounts are obvious because you can see the host path. Named volumes are easier to forget because Docker stores them under its own volume path.
Docker documents a tar-based pattern for backing up and restoring volumes by mounting the volume into a temporary container. That is useful when you need a portable export of a named volume.
Example volume export:
#!/usr/bin/env bash
set -euo pipefail
VOLUME="grafana_data"
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
OUT="/var/backups/homelab/staging/volume-exports/${VOLUME}-${STAMP}.tar.gz"
mkdir -p "$(dirname "$OUT")"
docker run --rm \
-v "${VOLUME}:/volume:ro" \
-v "$(dirname "$OUT"):/backup" \
alpine \
sh -c "cd /volume && tar czf /backup/$(basename "$OUT") ."If a named volume contains a live database, prefer a database dump. If it contains app uploads, config, or plugin data, an export can be appropriate. If it contains cache, exclude it.
Off-box beats clever local snapshots
A local ZFS snapshot, LVM snapshot, or copied directory is useful for fast rollback. It is not enough by itself.
Your backup target should survive at least one boring disaster:
- The mini PC SSD dies.
- A bad Compose change deletes a volume.
- Ransomware or a compromised container damages writable data.
- You rebuild the Proxmox host and realize the only backup was on the same disk.
Good off-box targets include:
| Target | Why use it | Watch out for |
|---|---|---|
| NAS or second server | Fast restores and no cloud egress surprise | Same building, same power events |
| USB drive rotated off the host | Cheap and simple | Easy to forget; needs encryption |
| Backblaze B2 or S3-compatible storage | Off-site and automation-friendly | Ongoing cost, lifecycle rules, credentials |
| Garage/MinIO on separate hardware | Self-hosted object storage control | Not off-site unless replicated elsewhere |
Some links in this post may be affiliate links. If you buy through them, I may earn a small commission at no extra cost to you. I only include products that fit the use case and tradeoffs discussed here.
Natural affiliate placements for this topic would be a small UPS, a reliable external SSD or HDD for local rotation, and a mini PC or NAS if the reader is building a second backup target. I would keep those as optional buyer notes rather than turning the post into a shopping list.
A practical restic job
Here is a simple restic-shaped job. Treat it as a starting point, not a drop-in script for every host.
#!/usr/bin/env bash
set -euo pipefail
export RESTIC_REPOSITORY="s3:s3.example.com/homelab-backups/compose-host-01"
export RESTIC_PASSWORD_FILE="/etc/homelab-backup/restic-password"
export AWS_ACCESS_KEY_ID_FILE="/etc/homelab-backup/s3-access-key"
export AWS_SECRET_ACCESS_KEY_FILE="/etc/homelab-backup/s3-secret-key"
BACKUP_ROOT="/var/backups/homelab/staging"
HOSTNAME_TAG="$(hostname -s)"
# Run database dump scripts first.
/usr/local/sbin/backup-postgres-payload
/usr/local/sbin/export-compose-volumes
restic backup \
--tag docker-compose \
--tag "$HOSTNAME_TAG" \
/srv/stacks \
/srv/appdata \
"$BACKUP_ROOT"
restic forget \
--keep-daily 14 \
--keep-weekly 8 \
--keep-monthly 12 \
--prune
restic check --read-data-subset=1GTwo important caveats:
- Store credentials in root-readable files, a secret manager, or your existing host secret pattern. Do not paste them directly into the script.
- Tune retention to your storage budget. Retention that nobody monitors eventually becomes a surprise bill or a full disk.
Restic and Kopia both support repository checks. Use them. A backup repository that never gets checked is just a hope with a timestamp.
Make restore a runbook, not a memory test
A backup is not done until the restore path is written down.
For each important app, keep a short restore runbook:
- Provision a clean VM or use a disposable test host.
- Install Docker and the Compose plugin.
- Clone the infrastructure repo.
- Restore
.envand secrets from the approved secret source. - Restore app data directories from backup.
- Restore database dumps with the matching database tool.
- Start the app on an isolated port or test hostname.
- Run a smoke test.
- Record what failed or what was missing.
Example Postgres restore:
createdb -h localhost -U "$POSTGRES_USER" "$POSTGRES_DB"
pg_restore -h localhost -U "$POSTGRES_USER" -d "$POSTGRES_DB" \
--clean --if-exists /restore/payload-20260723T120000Z.dumpFor a Compose app, the smoke test can be simple:
- The container starts without crash looping.
- The login page loads.
- A known record or uploaded file exists.
- Background jobs are not stuck.
- The reverse proxy can be pointed back after validation.
Monitoring the backup job
Cron email is better than nothing, but I prefer a positive heartbeat. If the backup is supposed to run every night, a monitoring system should complain when it does not run.
Useful signals:
| Signal | Why it matters |
|---|---|
| Last successful backup timestamp | Detects jobs that stopped running |
| Snapshot size changed unusually | Finds accidental exclusions or runaway data |
| Repository check result | Catches corruption and credential issues |
| Oldest available restore point | Confirms retention is behaving as expected |
| Restore drill date | Keeps the team honest, even if the team is just you |
This can be as simple as a Healthchecks.io ping, Uptime Kuma push monitor, Grafana annotation, or a small file exported to your existing monitoring stack.
What I would exclude by default
Do not pay to back up noise unless you have a reason.
Common exclusions:
- Transcode caches.
- Package manager caches.
- Download scratch directories.
- Rebuildable container images.
- Prometheus high-cardinality data with no long-term value.
- Temporary uploads that the app can regenerate.
- Media that already exists in another managed library or archive.
Be careful with broad exclusions. The safe process is to run a backup, list the snapshot contents, and confirm the important paths are present before adding more exclude rules.
The failure modes to design around
The backup design should handle boring failures, not movie-plot failures.
| Failure | Design response |
|---|---|
| Host disk dies | Off-box encrypted backup plus Git-managed Compose files |
| Bad app update corrupts data | Retention with multiple restore points |
| Deleted Docker volume | Volume export or file-level backup |
| Database copy is inconsistent | Native database dumps before file backup |
| Backup credentials leak | Least-privilege storage credentials and rotation |
| Backup silently stops | Positive monitoring heartbeat |
| Restore docs are stale | Scheduled restore drill |
If you can recover from those, you are ahead of most homelabs.
My recommended baseline
For a small but serious Compose homelab, I would start here:
- Git for Compose files, scripts, and runbooks.
- Encrypted restic or Kopia repository for app data and dumps.
- Native dumps for every important database.
- One local fast-restore target and one off-site target if the data matters.
- Daily automated backups with retention.
- Monthly lightweight restore test for the most important app.
- Monitoring that alerts on missing success, not just explicit failure.
That is enough to recover from the most likely failures without turning the homelab into a compliance program.
FAQ
Should I stop containers before backing up?
For simple file-based apps, a short maintenance stop can make backups cleaner. For databases, use database-native dumps regardless. If you have filesystem snapshot support and know the app's consistency requirements, you can get more advanced later.
Is a Proxmox VM backup enough?
It is useful, especially for fast full-machine restores. I still want app-aware database dumps and off-box copies. VM-level backups are convenient until you need one table, one upload directory, or a restore onto a different host layout.
Should I use restic or Kopia?
Either can work. Pick the one you will operate and test. Restic is simple and scriptable. Kopia has a broader built-in policy and UI story. The restore drill matters more than the logo.
Do I need cloud storage?
Not always. If the data is replaceable, a second local machine may be enough. If the data is personal, business-related, or painful to recreate, use an off-site target. Cloud object storage is one option; a rotated encrypted drive is another.
Sources and further reading
- Docker documentation: back up, restore, or migrate data volumes.
- PostgreSQL documentation:
pg_dumpandpg_restore. - Restic documentation: preparing repositories, backing up, retention, and checking repositories.
- Kopia documentation: snapshot and repository features.
- Backblaze B2 pricing page for current object storage pricing details.
Final thought
The best homelab backup system is boring. It runs quietly, alerts when it stops, and gives you a restore path that does not depend on panic-Googling at midnight.
Do not start by buying storage. Start by picking one important Compose app and proving you can rebuild it somewhere else. Then automate that path until it becomes routine.