Docker & ContainersXXII Β· Disaster RecoveryRebuild
Rebuilding a host from images plus volume backups
What you'll learn
- Bootstrap a replacement host with a pinned engine and restored daemon config
- Restore volume contents before the application ever starts
- Use external volumes so Compose refuses to start on missing state
- Time each phase to produce a measured RTO
Prerequisites
Verified against Docker Engine 29.x Β· Docker Engine 28.x Β· Docker Compose 2.x Β· containerd 2.x Β· runc 1.2.x Β· BuildKit 0.20+ Β· Linux kernel 5.15+ Β· Ubuntu 24.04 LTS Β· Debian 12 (Bookworm) Β· 2026-08-11
The host is gone. You have a registry, a git repository and a directory of volume archives. This lesson is the sequence that turns those three things back into a running service, and the one ordering mistake that makes it look like it worked when it did not.
Phase 1 β the substrate
flowchart LR
A[Provision host] --> B[Install pinned Docker]
B --> C[Restore daemon.json]
C --> D[Restore firewall rules]
D --> E[Registry credentials]
Pin the engine version
Restoring onto a newer engine than you lost is a change you are making during an outage, with no rollback and no test. Pin to the version that was running.
apt-cache madison docker-ce | head
VER='5:28.0.1-1~ubuntu.24.04~noble'
sudo apt-get install -y \
"docker-ce=$VER" "docker-ce-cli=$VER" containerd.io docker-compose-plugin
sudo apt-mark hold docker-ce docker-ce-cli containerd.io
docker version --format 'server={{.Server.Version}}'The version string is distribution-specific and long. Record the
exact string in the runbook rather than reconstructing it under
pressure β dpkg -l | grep docker-ce on the healthy host, today.
Restore configuration before starting anything
/etc/docker/daemon.json, any systemd drop-in under
/etc/systemd/system/docker.service.d/, and the firewall rules from
docker-user-chain-and-rule-persistence. All of these should come
from configuration management; if they come from a backup tarball,
restore them now, before the first container runs.
sudo dockerd --validate --config-file=/etc/docker/daemon.json
sudo systemctl restart docker
--validate parses the file and exits without starting the daemon,
which turns a typo into a message instead of a daemon that will not
come up.
Phase 2 β state before services
This is the phase where rebuilds go wrong, and the failure looks like a success.
Restore each volume with a helper container, exactly mirroring the backup:
VOL=app-postgres-data
ARCHIVE=/mnt/restore/app-postgres-data-2026-08-10.tar.gz
docker volume create "$VOL"
docker run --rm \
-v "$VOL":/target \
-v /mnt/restore:/restore:ro \
alpine:3.20 \
tar xzf "/restore/$(basename "$ARCHIVE")" -C /target
docker run --rm -v "$VOL":/target:ro alpine:3.20 ls -la /target | headMake Compose refuse to invent state
Declare data volumes as external. The compose specification is
explicit: with external: true Compose βwill not attempt to create
the volumeβ and instead returns an error if it does not exist.
services:
db:
image: postgres:16
volumes:
- app-postgres-data:/var/lib/postgresql/data
volumes:
app-postgres-data:
external: true
This converts the silent failure into a loud one. A rebuild that
forgot to restore a volume now fails at compose up with a missing
volume error, instead of starting an empty database and looking
healthy.
It is the single highest-value line in a production compose file for DR purposes, and it costs one word.
Phase 3 β start, then verify data rather than liveness
$ docker compose pull
docker compose up -d
docker compose ps --format 'table {{.Name}}\t{{.State}}\t{{.Status}}'NAME STATE STATUS
db running Up 41 seconds (healthy)
app running Up 38 seconds (healthy)
proxy running Up 38 secondsIllustrative output
Healthy means βthe process answered its own healthcheckβ. A freshly initialised empty database is healthy. Verify the data:
docker compose exec -T db psql -U app -t -c \
"SELECT count(*) FROM orders;"
docker compose exec -T db psql -U app -t -c \
"SELECT max(created_at) FROM orders;"
curl -fsS http://127.0.0.1:8080/healthz
curl -fsS http://127.0.0.1:8080/api/orders/latestThe timestamp of the newest record is your measured RPO for this
recovery. Compare it against the RPO you committed to in
docker-rpo-rto. If the newest order is nineteen hours old and the
commitment was four, the backup schedule is the finding β not the
restore procedure.
Timing the phases
Record the clock at each phase boundary. The total is your measured RTO, and the per-phase split tells you where to spend effort.
{
echo "start $(date --iso-8601=seconds)"
echo "host ready $(date --iso-8601=seconds)"
echo "volumes restored $(date --iso-8601=seconds)"
echo "stack up $(date --iso-8601=seconds)"
echo "verified $(date --iso-8601=seconds)"
} | tee /var/log/dr-drill-timing.txt
A typical first drill splits roughly: 20% provisioning, 60% volume restore, 20% verification. That distribution says the lever is transfer speed and archive size β not a faster runbook.
- Provision and pin. Same OS, same engine version, held packages.
- Restore host configuration first. daemon.json, systemd drop-ins, firewall rules. Validate before starting the daemon.
- Authenticate to the registry using break-glass credentials that do not live on the lost host.
- Create and populate every volume, before any application container exists.
- Check UID ownership against the image you are about to run.
- Bring the stack up with data volumes declared external, so a missing volume is an error.
- Verify data, not liveness. Row counts, newest record, an end-to-end read.
- Record the timings and the newest-record timestamp. Those are your measured RTO and RPO.
- Cut DNS or the load balancer over last, only after verification passes.
Sanity check
Knowledge check Β· 4 questions
Q1. During a rebuild an engineer runs docker compose up -d before restoring the volume archives. What is the most likely outcome?
Q2. A restored volume has correct contents but Postgres fails at startup with a permission error on its data directory. What is the most likely cause?
Q3. Which checks belong to verification, as opposed to simply observing that the stack started? Select all that apply.
Q4. Declaring a data volume as external: true in the compose file makes a rebuild that forgot to restore it fail loudly instead of starting empty.
Passing score: 75%. Answers are checked in this browser.