Introduction
This hands-on guide shows how to back up and restore Docker images, volumes, and configuration using practical, copy-paste examples. You will get an end-to-end workflow, modern best practices, deeper examples (including databases), and a clear disaster recovery drill you can run on a clean host. By the end, you will have a repeatable approach you can use on a laptop and extend to production.
Prerequisites and assumptions
- Linux host with Docker Engine 20.10+ (24.x recommended) and Docker Compose v2
- Shell with bash, GNU tar, and sha256sum available
- User in the docker group (or use sudo before docker commands)
- At least 2x the size of your data free on the backup target
- Optional but recommended: encryption (age, gpg, or openssl) and offsite/object storage
- Images are pinned by tag or digest (avoid latest) and Compose files are in git
Tip: use a clean VM or a Docker Desktop context to test restores safely.
Workflow overview
- Inventory what to protect
- Images and exact tags or digests
- Container definitions (Compose files, Dockerfiles, env files)
- Volumes and bind mounts that hold state
- Databases inside containers (take logical dumps, not raw file copies)
- Secrets and keys (store separately or encrypt)
- Quiesce or snapshot
- Put apps in maintenance mode or schedule short downtime to pause writes
- For databases: use logical dumps (pg_dump, mysqldump) or storage snapshots designed for consistency
- Back up
- Config: git-managed files, plus a resolved Compose lock export
- Images: docker save to a tar archive
- Volumes: tar via a temporary helper container
- Databases: dump to files stored with the backup
- Store and protect
- Create checksums (sha256)
- Encrypt at rest and in transit
- Keep offsite copies with versioned retention and lifecycle policies
- Validate
- Test a restore on a clean host
- Verify container health, data checksums, logs, and app endpoints
- Automate
- Shell scripts or Makefile with clear logs and non-zero exits on failure
- Schedule via cron, systemd timers, or CI/CD runners
- Document rollback
- Exact steps to return to a known-good backup, who approves, and how to verify success
Quick artifact map
| Artifact | How to back up | How to verify |
|---|---|---|
| Images | docker save -o images.tar repo:tag | sha256sum -c images.tar.sha256; docker load -i images.tar |
| Volumes | Tar from a helper container | sha256sum -c volume.sha256; tar tzf volume.tar.gz |
| Compose config | docker compose config > compose.lock.yaml | Diff with expected; track in git |
| DB dumps | pg_dump/mysqldump to files | Inspect headers; test import into a temp DB |
Practical backups (copy-paste)
The example backs up a simple Nginx site that serves content from a named volume. Adapt the pattern for your services.
Set up a demo service
# Network and volume
docker network create appnet
docker volume create webdata
# Run Nginx pinned to a tag (avoid latest)
docker run -d --name web \
--network appnet -p 8080:80 \
-v webdata:/usr/share/nginx/html:rw \
nginx:1.25
# Put sample content in the volume
docker run --rm -v webdata:/data alpine \
sh -c "echo 'Hello from backup demo' > /data/index.html"
Back up the volume with a helper container
set -euo pipefail
mkdir -p backups
STAMP=$(date +%F_%H%M%S)
VOL_ARCHIVE="backups/webdata-$STAMP.tar.gz"
VOL_SUM="${VOL_ARCHIVE}.sha256"
TMP_VOL_ARCHIVE="${VOL_ARCHIVE}.tmp"
# Read-only mount of the volume; preserve ownership/permissions
docker run --rm \
-v webdata:/data:ro \
-v "$(pwd)/backups":/backup \
alpine sh -c "tar --numeric-owner -czf /backup/$(basename \"$TMP_VOL_ARCHIVE\") -C /data ."
mv "$TMP_VOL_ARCHIVE" "$VOL_ARCHIVE"
sha256sum "$VOL_ARCHIVE" > "$VOL_SUM"
For bind mounts on the host, prefer rsync -aHAX to preserve metadata, or tar the directory directly from the host.
Back up the image (restore without pulling)
IMG_ARCHIVE="backups/nginx-1.25-images-$STAMP.tar"
TMP_IMG_ARCHIVE="${IMG_ARCHIVE}.tmp"
docker pull nginx:1.25
docker save -o "$TMP_IMG_ARCHIVE" nginx:1.25
mv "$TMP_IMG_ARCHIVE" "$IMG_ARCHIVE"
sha256sum "$IMG_ARCHIVE" > "${IMG_ARCHIVE}.sha256"
Back up container definition and Compose
# Capture the running container inspect (ports, mounts, env)
docker inspect web > "backups/web-$STAMP.inspect.json"
# If you use Compose, export a resolved lock (run in the project dir)
# docker compose config > "backups/compose-$STAMP.lock.yaml"
Optional: database dumps (Postgres and MySQL)
# Postgres logical dump example
# export PGPASSWORD=yourpassword
# docker exec -e PGPASSWORD=$PGPASSWORD pg \
# pg_dump -U $PGUSER -h localhost -F c -f /tmp/db.dump $PGDATABASE
# docker cp pg:/tmp/db.dump "backups/pg-$STAMP.dump"
# MySQL logical dump example
# docker exec mysql \
# sh -c 'mysqldump -u$MYSQL_USER -p$MYSQL_PASSWORD $MYSQL_DATABASE > /tmp/db.sql'
# docker cp mysql:/tmp/db.sql "backups/mysql-$STAMP.sql"
Good practice: dump to a temporary path and move to the final filename only after success to avoid partial artifacts.
Practical restore and verification
Always start by restoring on a clean host or throwaway VM. Treat it as a disaster recovery drill.
Load images
# Verify and load the saved image
sha256sum -c backups/nginx-1.25-images-*.tar.sha256
IMG_TAR=$(ls -1 backups/nginx-1.25-images-*.tar | tail -n1)
docker load -i "$IMG_TAR"
Recreate and restore the volume
# Create the target volume
NEW_VOL=webdata
docker volume create "$NEW_VOL"
# Pick the latest archive and verify
sha256sum -c backups/webdata-*.sha256
VOL_ARCHIVE=$(ls -1 backups/webdata-*.tar.gz | tail -n1)
# Extract into the empty volume
docker run --rm \
-v "$NEW_VOL":/data \
-v "$(pwd)/backups":/backup \
alpine sh -c "cd /data && tar xzf /backup/$(basename \"$VOL_ARCHIVE\")"
Start the container and validate
docker run -d --name web-restore \
-p 8080:80 \
-v "$NEW_VOL":/usr/share/nginx/html \
nginx:1.25
# Basic health check
curl -fsS http://localhost:8080 | tee /tmp/page.html
grep -q "Hello from backup demo" /tmp/page.html && echo "OK" || (echo "FAIL"; exit 1)
# Optional deeper checks: logs and container health
docker logs --tail=50 web-restore
If you use Compose, restore with the locked config
# In the project directory that holds compose-$STAMP.lock.yaml
# docker compose -f backups/compose-$STAMP.lock.yaml up -d
# docker compose ps
Disaster recovery drill: step-by-step
Run this quarterly (or after significant changes) and record timing to validate RTO/RPO.
- Provision a clean VM or Docker context with enough disk space.
- Copy the latest backup set (images, volume tars, DB dumps, compose lock, checksums).
- Verify checksums for all artifacts with
sha256sum -c. docker loadimages; confirm tags/digests match expectations.- Create fresh named volumes; never restore into a used volume.
- Restore volume tarballs using a helper container.
- Recreate secrets/env files (decrypt if needed) and validate permissions.
- Start services (Compose or docker run) and wait for healthchecks.
- Verify:
- App endpoint(s) respond with expected content or status.
- Logs are free of repeated errors.
- Database basic integrity (e.g., run
SELECT COUNT(*)on key tables ormysqlcheck).
- Document exact commands, timings, and outcomes. Fix gaps before the next drill.
Local pilot plan
- Scope: One Nginx service with a named volume.
- Artifacts: image tar, volume tar.gz, inspect JSON, Compose lock (if used), checksums.
- Success criteria: RTO under 5 minutes on a laptop; checksum validation passes; homepage text matches expected.
- Steps:
- Implement the backup script from the examples.
- Schedule a daily cron job to write into ./backups.
- Run a restore on a fresh VM or Docker Desktop context.
- Record the process, timings, and results in your runbook.
- Next iteration: add a small Postgres container with
pg_dump; move scripts behind Make targets; add encryption and offsite sync.
Validation checklist and useful commands
- Pre-backup: containers healthy (
docker ps), app endpoints OK (curl -fsSI), disk space headroom (df -h). - Artifact checks:
sha256sumfor all tars; list tar contents (tar tzf backups/…tar.gz | head -n10). - Post-restore: service responds; logs clean; run a smoke test or integration test script.
Failure modes and how to recover
| Symptom | Likely cause | Fix |
|---|---|---|
tar: write error: No space left on device | Insufficient disk space | Free space or change backup target; verify with df -h |
sha256sum: FAILED | Corrupted or partial artifact | Recreate/recopy; write to a temp file then rename atomically |
mysqldump/pg_dump permission errors | Wrong credentials or roles | Pass correct env vars; grant a read-only backup role |
port already allocated | Old container still bound | Stop/remove old container or remap ports |
| Volume not empty on restore | Restoring into a used volume | Create a new volume; never mix old and new data |
| Permission denied on bind mount | SELinux/AppArmor or host perms | Use :z/:Z for SELinux; fix ownership; prefer named volumes |
docker load invalid tar | Wrong file or truncated transfer | Validate checksum; recopy from source |
| App crash loop after restore | Missing env/secret or version mismatch | Restore env/secrets; ensure image and data versions are compatible |
Rollback planning and cautions
- Keep at least 3 recent backup sets per service; label with timestamp, environment, and app git commit.
- Pin images to immutable tags or digests so rollback is deterministic.
- Never roll back databases without considering schema migrations; keep a current logical dump and test compatibility.
- During rollback, stop workloads first, restore the known-good set, then start and validate before reopening traffic.
- Keep secrets outside of general backups or encrypt them; regularly test decryption.
Example:
# Stop the faulty release
docker compose down # or: docker stop <containers>
# Restore previous artifacts (images and volumes) as in the restore section
# Start known-good images explicitly
docker run -d --name app \
-v appdata:/var/lib/app \
myorg/app:1.2.3
# For Compose, pin and start
docker compose -f compose-rollback.lock.yaml up -d
Automation examples
Cron scheduling (daily at 02:15):
# Edit with: crontab -e
15 2 * * * cd /opt/myapp && ./backup.sh >> backup.log 2>&1
Simple Makefile targets:
backup:
@./scripts/backup.sh
restore-latest:
@./scripts/restore.sh latest
verify:
@./scripts/verify.sh
clean-old:
@find backups -type f -mtime +30 -delete
Ensure scripts use set -euo pipefail, exit non-zero on failure, and store logs alongside artifacts. Consider a systemd timer or CI/CD schedules for centralized visibility.
Notes for related platforms
- Docker Compose: commit compose files to git; create a compose lock via
docker compose config; back it up with checksums. Use healthchecks and pinned versions. - Kubernetes: back up manifests (git), use CSI volume snapshots or logical DB dumps, and test restores in a separate namespace. Validate Deployments roll out and Pods become Ready.
- CI/CD: run backup scripts in a scheduled pipeline, store artifacts in your object storage, and keep credentials in protected variables with rotation.
Conclusion
Backups only matter if restores work. You now have a practical, operator-focused workflow to back up and restore Docker images, volumes, and configuration, plus a clean recovery drill you can run today. Start small, pin image versions or digests, add checksums and encryption, and practice a full restore on a fresh host. As you scale to Compose stacks or Kubernetes, the same principles hold: inventory state, quiesce, back up, validate, and document rollback.