## 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

<div class="my-stack-md overflow-x-auto">
<table class="w-full min-w-[42rem] border-collapse text-left">
<thead><tr><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Artifact</th><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">How to back up</th><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">How to verify</th></tr></thead>
<tbody><tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Images</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">docker save -o images.tar repo:tag</code></td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">sha256sum -c images.tar.sha256</code>; <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">docker load -i images.tar</code></td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Volumes</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Tar from a helper container</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">sha256sum -c volume.sha256</code>; <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">tar tzf volume.tar.gz</code></td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Compose config</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">docker compose config &gt; compose.lock.yaml</code></td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Diff with expected; track in git</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">DB dumps</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">pg_dump</code>/<code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">mysqldump</code> to files</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Inspect headers; test import into a temp DB</td></tr></tbody>
</table>
</div>

## 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 load images; 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 or mysqlcheck ).

- 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: sha256sum for 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

<div class="my-stack-md overflow-x-auto">
<table class="w-full min-w-[42rem] border-collapse text-left">
<thead><tr><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Symptom</th><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Likely cause</th><th scope="col" class="border border-outline-variant bg-surface-container-low px-4 py-3 text-left font-label-md font-semibold text-on-surface">Fix</th></tr></thead>
<tbody><tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">tar: write error: No space left on device</code></td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Insufficient disk space</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Free space or change backup target; verify with <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">df -h</code></td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">sha256sum: FAILED</code></td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Corrupted or partial artifact</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Recreate/recopy; write to a temp file then rename atomically</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">mysqldump/pg_dump</code> permission errors</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Wrong credentials or roles</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Pass correct env vars; grant a read-only backup role</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">port already allocated</code></td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Old container still bound</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Stop/remove old container or remap ports</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Volume not empty on restore</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Restoring into a used volume</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Create a new volume; never mix old and new data</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Permission denied on bind mount</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">SELinux/AppArmor or host perms</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Use <code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">:z</code>/<code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">:Z</code> for SELinux; fix ownership; prefer named volumes</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant"><code class="font-mono text-[0.9em] bg-surface-container px-1 py-0.5 rounded">docker load</code> invalid tar</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Wrong file or truncated transfer</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Validate checksum; recopy from source</td></tr>
<tr><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">App crash loop after restore</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Missing env/secret or version mismatch</td><td class="border border-outline-variant px-4 py-3 align-top text-body-md text-on-surface-variant">Restore env/secrets; ensure image and data versions are compatible</td></tr></tbody>
</table>
</div>

## 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.