Intro
Backups are useless until you can restore them. This guide shows how to back up and restore Docker Compose applications with reliable, repeatable steps. You will learn what to back up, how to capture data from common services like PostgreSQL, Redis, and Nginx, how to validate restores, and how to plan for disaster recovery and rollback. Commands are designed to run safely on a developer laptop or a small production host.
Workflow Overview
A simple, repeatable workflow reduces mistakes:
- Inventory: list what holds state (volumes, bind mounts, databases, app configs, secrets).
- Quiesce when needed: pause writes or use service-native dump tools.
- Backup: capture data and configs to a timestamped archive.
- Store: save to durable, off-host storage.
- Verify: checksum and test-restore in a throwaway environment.
- Document: record exact commands, versions, and locations.
- Automate: script the steps and schedule them.
- Review: rehearse a full restore regularly.
What To Back Up
In Docker Compose, back up:
- Compose files: docker-compose.yml, compose.override.yml, and .env.
- Service configs: Nginx conf, app config files, TLS certs.
- Named volumes: databases and any service state (e.g., pgdata, redisdata).
- Bind-mounted data directories: content served by Nginx or app uploads.
- Secrets and credentials: stored securely, not in images.
- Version info: image tags and plugin versions.
Tip: run docker compose config to expand and inspect the effective configuration.
Backup Strategies
Below is a minimal stack and practical backup commands. Adjust names to your project.
Example docker-compose.yml:
version: "3.9"
services:
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: app
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redisdata:/data
nginx:
image: nginx: alpine
ports:
- "8080:80"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d: ro
- ./site:/usr/share/nginx/html: ro
volumes:
pgdata:
redisdata:
General setup:
mkdir -p backups
TS=$(date -u +%Y%m%d_%H%M%S)
- Back up Compose files and bind-mounted content:
# Compose files and env
tar czf backups/compose_${TS}.tgz docker-compose.yml .env || exit 1
# Nginx config and site content (bind mounts)
tar czf backups/nginx_${TS}.tgz nginx/conf.d site || exit 1
- PostgreSQL: prefer a logical dump for consistency while running.
# Logical dump (recommended)
PGPASSWORD=secret docker compose exec -T db pg_dump -U app -d app | gzip > backups/pg_app_${TS}.sql.gz
If you must snapshot the data directory (ensure no writes or stop db):
# Riskier: filesystem snapshot. Quiesce or stop first.
# docker compose stop app-writers ...
docker compose exec -T db sh -c 'tar czf - /var/lib/postgresql/data' > backups/pgdata_${TS}.tgz
- Redis: trigger a background save, then archive data.
# Ensure an up-to-date dump (AOF or RDB present)
docker compose exec redis redis-cli SAVE
# Archive the data directory
docker compose exec -T redis sh -c 'tar czf - /data' > backups/redis_${TS}.tgz
- Optional: checksums for integrity.
(cd backups && sha256sum *.tgz *.gz > SHA256SUMS_${TS}.txt)
- Off-host copy: use your storage of choice (secure, encrypted). Keep multiple generations with retention.
Restore Strategies
Always restore into a disposable environment first.
- Restore Compose files and bind mounts:
# Replace or stage into a fresh directory, then:
tar xzf backups/compose_${TS}.tgz
mkdir -p nginx/conf.d site
tar xzf backups/nginx_${TS}.tgz -C .
- Start core services:
docker compose up -d db redis nginx
- Restore PostgreSQL from logical dump:
gunzip -c backups/pg_app_${TS}.sql.gz | docker compose exec -T db psql -U app -d app
If you backed up the raw data directory (match the same Postgres major version):
docker compose stop db
# Clear the existing data directory inside the container
docker compose run --rm db sh -c 'rm -rf /var/lib/postgresql/data/*'
# Extract the archive back to the absolute path included in the tar
cat backups/pgdata_${TS}.tgz | docker compose run --rm -T db sh -c 'tar xzf - -C /'
# Start database
docker compose up -d db
- Restore Redis:
docker compose stop redis
# Clear and restore
docker compose run --rm -T redis sh -c 'rm -rf /data/* && tar xzf - -C /' < backups/redis_${TS}.tgz
docker compose up -d redis
- Reload Nginx if configs changed:
docker compose exec nginx nginx -t && docker compose exec nginx nginx -s reload
Note: Project-scoped volume names may include a prefix. Using exec/run inside the service avoids guessing the volume name.
Validation Checks
After restore, verify correctness before serving traffic:
- Containers healthy:
docker compose ps
docker compose logs --since=2m db redis nginx
- PostgreSQL sanity:
docker compose exec -T db psql -U app -d app -c "select count(*) as tables from information_schema.tables where table_schema='public';"
- Redis sanity:
docker compose exec -T redis redis-cli ping
docker compose exec -T redis redis-cli dbsize
- Nginx responds:
curl -I http://localhost:8080/
- Data spot checks: known rows, files, and user accounts exist.
- Checksums: if you stored SHA256SUMS, verify them:
(cd backups && sha256sum -c SHA256SUMS_*.txt)
- Access and permissions: app can read/write where needed.
Disaster Recovery and Rollback
Plan for the worst so recovery is calm and fast:
- Recovery point objective (RPO): how much data loss is acceptable. Set backup frequency accordingly.
- Recovery time objective (RTO): how quickly you must be back online. Pre-stage images and scripts.
- Storage: keep encrypted copies off-host and off-region if critical.
- Version pinning: match image major versions to avoid incompatible data files.
- Rollback plan: keep at least one previous snapshot ready. If a change fails, restore the last known good backup and restart services.
- Staged restore: bring up a parallel stack with a different project name:
# Use a different project to avoid clobbering prod volumes
export COMPOSE_PROJECT_NAME=stack_restore_test
docker compose up -d
- Runbook: document exact commands, file locations, and success criteria. Practice on a schedule.
Local Pilot Plan
Start small so you can measure success quickly:
Scope:
- Services: PostgreSQL and Redis only.
- Data: a seed SQL file that creates a few tables and rows; a few Redis keys.
Steps:
- Bring up the stack, load seed data.
- Run the backup commands to produce timestamped archives.
- Tear down the stack and remove volumes.
- Restore into a fresh stack using the archives.
- Validate with SQL queries and Redis dbsize; check logs.
Success criteria (measurable):
- Backup duration under 2 minutes for seed data.
- Restore completes with zero errors in logs.
- Table and row counts match exactly; Redis key count matches.
- Commands scripted into a single backup.sh and restore.sh that return 0 on success.
This narrow, measurable pilot lets you inspect results locally and build confidence before expanding scope.
Common Mistakes
Avoid these pitfalls:
- Backing up containers instead of data: containers are ephemeral; back up volumes, bind mounts, and dumps.
- Skipping logical dumps for databases: filesystem tars of live databases can be inconsistent.
- Forgetting .env and config files: restores then fail or differ from expected.
- Not testing restores: untested backups often fail when needed most.
- Version drift: restoring a Postgres 16 data dir into Postgres 15 (or vice versa) will fail.
- Restoring into the wrong project: unintentionally creates new volumes; validate COMPOSE_PROJECT_NAME.
- Missing permissions and ownership: ensure tar preserves mode and user when relevant.
- No retention policy: old backups overwrite new, or storage fills up.
- Ignoring exit codes: pipe failures or partial archives go unnoticed.
- Leaving services writing during backup: quiesce or use native dump tools.
Conclusion
Backups are only as good as your ability to restore them quickly and confidently. Define what to protect, use service-native tools for consistent data, archive configs and volumes, and validate with concrete checks. Start with a small pilot, measure results, and automate once you trust the process. Keep versions pinned, store backups off-host, and rehearse a full restore so you can execute it when it matters.