Skip to main content
RunBook Academy

Docker & ContainersXXII Β· Disaster RecoveryRPO and RTO

RPO and RTO β€” defining recovery objectives

Advanced⏱ ~24 mindocker

What you'll learn

  • Derive RPO and RTO from cost per hour rather than from what sounds reassuring
  • Decompose an RTO into its components and find which one dominates
  • Match a single-host Docker architecture to the objectives it can actually meet
  • Recognise the four ways a stated RPO is quietly wrong

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

Not yet marked complete on this device.

Two definitions, and they are not symmetric:

  • RPO β€” Recovery Point Objective. The maximum acceptable amount of data loss, expressed in time. RPO 1 hour means that after a disaster you may be missing up to an hour of work, and that is acceptable.
  • RTO β€” Recovery Time Objective. The maximum acceptable outage, expressed in time. RTO 4 hours means the service is expected back within four hours.

The asymmetry that matters: RPO is bought with backup frequency, RTO is bought with infrastructure, and the two are unrelated purchases. Backing up every five minutes improves RPO to five minutes and improves RTO by exactly nothing β€” you still have to provision a host, pull images and replay a dump. Teams who believe more frequent backups make recovery faster are budgeting for the wrong thing, and they find out during a drill or an outage.

Deriving the numbers instead of picking them

β€œRPO 1 hour, RTO 4 hours” appears in a great many documents and is usually a number that sounded responsible. The derivation is not difficult and it changes the answer.

Decomposing RTO for a single Docker host

RTO is not one number, it is a sum, and the sum is dominated by one or two terms that are rarely the ones people optimise.

PhaseTypical, unpreparedTypical, preparedWhat changes it
Detect5–30 min1–2 minAlerting on the service, not on the host
Decide to fail over10–60 min2 minA written trigger condition and a named decider
Provision a host30–120 min0 (already running)Warm standby, or an image and IaC
Install and configure the engine15–40 min0Pre-built host image with daemon.json
Pull images5–30 min0Pre-pulled, or a local registry mirror
Restore volume data20 min – 6 h5–20 minVolume size, transfer rate, restore parallelism
Start the stack and reach healthy2–15 min2–15 minHealth check start periods, migrations
Repoint DNS or the load balancer1 min – 1 h1–5 minTTL, and whether the change is automated
Verify10–30 min5 minA written content assertion, not a look around

Two lines deserve attention because they are consistently underestimated.

Decide to fail over is often the largest term in a real incident and appears in no plan. The host stopped responding at 02:14; at 02:47 the on-call engineer is still trying to determine whether it will come back, because failing over is irreversible-ish and nobody wants to be the person who did it unnecessarily. Thirty-three minutes of RTO consumed by an absent decision rule. The fix is a written trigger: if the host has not responded to SSH for 10 minutes and the out-of-band console shows no progress, we fail over. Put a name against it.

Repoint DNS is bounded below by your record’s TTL, and resolvers routinely ignore short TTLs. A 3,600-second TTL is an hour of RTO you cannot recover by working faster. Lower it to 60 seconds now, permanently, as a standing decision; the extra query volume is negligible and it converts an hour into a minute.

Read-only / Safemeasure your own terms
STACK=/srv/app

# How long does the stack take from cold to all-healthy?
docker compose -f "$STACK/compose.yaml" down
time docker compose -f "$STACK/compose.yaml" up -d --wait

# How much data has to move?
docker system df -v | sed -n '/Local Volumes/,$p'

# What is the DNS TTL you are actually bound by?
dig +noall +answer app.example.com | awk '{print $2, $1}'

docker compose up -d --wait blocks until every service with a health check reports healthy, and returns non-zero if any does not. That makes it a measurement rather than a guess: the number it prints is the β€œstart the stack and reach healthy” row of your table, on this host, today.

Read-only / Safemeasured, not estimated
$ time docker compose -f /srv/app/compose.yaml up -d --wait
[+] Running 5/5
βœ” Network app_default      Created
βœ” Container app-db         Healthy
βœ” Container app-redis      Healthy
βœ” Container app-web        Healthy
βœ” Container app-caddy      Healthy

real    3m48.114s
user    0m0.412s
sys     0m0.180s

Illustrative output

Three minutes forty-eight, with images already present and volumes already populated. Any RTO commitment shorter than that is arithmetically impossible before a single byte has been restored, and this is a four-minute experiment.

What a single Docker host can actually promise

ArchitectureAchievable RPOAchievable RTOHonest description
One host, nightly dump offsite24 h4–8 hAdequate for internal tools
One host, hourly dump offsite1 h4–8 hRPO improved, RTO untouched
One host, WAL archiving offsite5 min4–8 hCheapest large RPO improvement available
Two hosts, cold standby provisioned5 min – 1 h30–60 minProvisioning and engine install removed
Two hosts, warm standby, volumes synced hourly5 min15–30 minData restore mostly removed
Two hosts, streaming DB replica, manual promotionSeconds5–15 minThe realistic floor for standalone Docker
Anything claiming automatic failoverβ€”β€”Not standalone Docker. See the HA part.

The last row is the boundary. Standalone Docker has no mechanism that notices a host has died and starts the workload elsewhere. restart: always restarts a container on a host that is still running its daemon; it does nothing at all when the host is gone. An RTO in single-digit minutes therefore requires a human who is awake, or an orchestrator. Committing to one without either is committing to a coin flip on whether the outage starts during working hours.

Read-only / Safethe RPO you actually have
export RESTIC_REPOSITORY='rest:http://backup-01.example.com:8000/app-01'
export RESTIC_PASSWORD_FILE=/etc/restic/password

# Newest snapshot in the OFFSITE repository, per backup path
restic snapshots --host app-01 --latest 1

# Same thing as an age in hours, for a monitoring check that can fail
newest=$(restic snapshots --host app-01 --latest 1 --json | jq -r '.[0].time')
age_h=$(( ( $(date +%s) - $(date -d "$newest" +%s) ) / 3600 ))
echo "newest offsite snapshot is ${age_h}h old"
[ "$age_h" -le 24 ] || { echo 'FAIL: offsite copy exceeds stated RPO' >&2; exit 1; }

Run that and compare the output to the RPO in your documentation. The gap between them is your actual exposure, and it is usually a surprise.

Knowledge check

Knowledge check Β· 5 questions

  1. Q1. A team moves from nightly backups to hourly backups. What happens to RPO and RTO?

  2. Q2. The nightly dump starts at 02:00 and finishes at 03:30. The offsite sync runs at 06:00. The host is lost at 04:00. What is the actual data loss?

  3. Q3. Which RTO components can be reduced to near zero without buying additional hardware? Select all that apply.

  4. Q4. Which command turns the "start the stack and reach healthy" RTO component from an estimate into a measurement?

  5. Q5. A system whose database has an RPO of one hour and whose uploads volume is copied weekly has an RPO of one hour.

Passing score: 75%. Answers are checked in this browser.