8 min read

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:

  1. Keep Compose files, environment templates, and runbooks in Git.
  2. Dump databases with database-native tools before backing up their files.
  3. Back up application data directories and named volumes with a deduplicating backup tool.
  4. Copy backups to at least one off-box target.
  5. 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 typeExamplesBest backup approachRestore risk
Compose definitionscompose.yml, override files, systemd unitsGit repositoryLow if secrets are handled separately
Secrets and env.env, API keys, app secretsPassword manager, encrypted secret store, or encrypted backupHigh if missing or stale
DatabasesPostgres, MariaDB, Redis persistenceNative dumps plus optional file backupHigh if copied while inconsistent
App uploadsCMS media, documents, config uploadsFile backup or object storage replicationMedium to high
Docker named volumesApp data hidden under Docker volume pathsDocker volume tar/export or mount-aware backupMedium
Large replaceable mediaLinux ISOs, transcoding cache, downloadsUsually exclude or back up separatelyLow unless unique
Observability dataPrometheus, logs, uptime historyUsually short retention or selective backupLow 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:

Code snippettext
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 file

Restic 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_dump for one database, or pg_dumpall when roles and globals matter.
  • MariaDB/MySQL: mariadb-dump or mysqldump, 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:

Code snippetbash
#!/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 -T avoids pseudo-TTY issues in cron.
  • --format=custom gives you a compressed archive that works well with pg_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:

Code snippetbash
#!/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:

TargetWhy use itWatch out for
NAS or second serverFast restores and no cloud egress surpriseSame building, same power events
USB drive rotated off the hostCheap and simpleEasy to forget; needs encryption
Backblaze B2 or S3-compatible storageOff-site and automation-friendlyOngoing cost, lifecycle rules, credentials
Garage/MinIO on separate hardwareSelf-hosted object storage controlNot 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.

Code snippetbash
#!/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=1G

Two 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:

  1. Provision a clean VM or use a disposable test host.
  2. Install Docker and the Compose plugin.
  3. Clone the infrastructure repo.
  4. Restore .env and secrets from the approved secret source.
  5. Restore app data directories from backup.
  6. Restore database dumps with the matching database tool.
  7. Start the app on an isolated port or test hostname.
  8. Run a smoke test.
  9. Record what failed or what was missing.

Example Postgres restore:

Code snippetbash
createdb -h localhost -U "$POSTGRES_USER" "$POSTGRES_DB"
pg_restore -h localhost -U "$POSTGRES_USER" -d "$POSTGRES_DB" \
  --clean --if-exists /restore/payload-20260723T120000Z.dump

For 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:

SignalWhy it matters
Last successful backup timestampDetects jobs that stopped running
Snapshot size changed unusuallyFinds accidental exclusions or runaway data
Repository check resultCatches corruption and credential issues
Oldest available restore pointConfirms retention is behaving as expected
Restore drill dateKeeps 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.

FailureDesign response
Host disk diesOff-box encrypted backup plus Git-managed Compose files
Bad app update corrupts dataRetention with multiple restore points
Deleted Docker volumeVolume export or file-level backup
Database copy is inconsistentNative database dumps before file backup
Backup credentials leakLeast-privilege storage credentials and rotation
Backup silently stopsPositive monitoring heartbeat
Restore docs are staleScheduled 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_dump and pg_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.