Skip to main content
RunBook Academy

Docker & ContainersXXI Β· BackupOffsite

Offsite copies and the 3-2-1 rule

Intermediate⏱ ~22 mindocker

What you'll learn

  • Apply 3-2-1 to a single-host Docker stack and identify where it is violated
  • Distinguish a second copy from an independent copy using a blast-radius test
  • Configure an append-only or object-locked backup target
  • Choose a retention policy that survives a slow-burn corruption as well as a fast deletion

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.

The rule is old enough to be furniture:

  • 3 copies of the data.
  • 2 different media or storage types.
  • 1 copy offsite.

It is good advice and it is routinely satisfied on paper by an arrangement that one command can destroy. This lesson is about the test the rule does not contain.

The blast-radius test

For each copy of your data, ask: what single event destroys it? Then look for events that appear in more than one row.

Here is the arrangement most single-host Docker deployments actually have.

CopyWhereDestroyed by
Live dataapp_pgdata volume on host-01Host loss, docker volume rm, ransomware on host-01
Nightly tar/backup on host-01Host loss, disk failure, ransomware on host-01
restic repositoryS3, credentials in /etc/restic on host-01Ransomware on host-01 β€” the credentials are right there

Three copies. Two media. One offsite. It satisfies 3-2-1 completely, and a single compromise of host-01 destroys all three, because the third copy is deletable by a credential stored on the first.

That is the failure mode 3-2-1 was written before, and it is now the common one. A ransomware operator’s first action after gaining a foothold is to enumerate and delete backups β€” the encryption is worthless to them if you can restore. They look in /etc/restic, ~/.aws/credentials, /root/.borg-passphrase and the crontab, and they find them, because that is where a working backup job keeps them.

Making the third copy independent

Three mechanisms, in increasing order of strength. Pick based on what your backend supports.

Append-only credentials

The backup client can write new data and read existing data, but not delete. restic supports this directly via a REST server, and Borg via borg serve --append-only over SSH.

Configuration changeappend-only restic server
# On the BACKUP host. Clients get write+read, never delete.
docker run -d --name restic-server --restart unless-stopped -p 8000:8000 -v /srv/restic-repos:/data -v /srv/restic/.htpasswd:/data/.htpasswd:ro restic/rest-server:latest --append-only --private-repos --path /data
Read-only / Safeprove it is append-only
export RESTIC_REPOSITORY='rest:http://backup-01.example.com:8000/app-01'
export RESTIC_PASSWORD_FILE=/etc/restic/password

# Pick any snapshot and attempt to remove it
SNAP=$(restic snapshots --json | head -c 200)
echo "$SNAP"

# This is the verification. A zero exit status here means the
# append-only property is NOT in effect and the design has failed.
if restic forget --keep-last 1 --prune; then
echo 'FAIL: production host can delete backup history' >&2
exit 1
else
echo 'PASS: deletion refused by the server'
fi
Read-only / Safethe refusal you want
$ restic forget --keep-last 1 --prune
Applying Policy: keep 1 latest snapshots
remove 41 snapshots:
ID        Time                 Host        Tags
--------------------------------------------------
7c1a9e04  2026-08-11 02:00:14  app-01      app_uploads
...
--------------------------------------------------

Save(<lock/9d3f2a71bc>) returned error, retrying after 552.330144ms:
server response unexpected: 403 Forbidden (403)
Fatal: unable to create lock in backend: repository is in append-only mode

PASS: deletion refused by the server

Illustrative output

Note that restic gets as far as listing what it would remove before the server refuses. Reading only the first half of that output β€” 41 snapshots queued for deletion β€” and concluding the protection failed is an easy mistake. The 403 Forbidden and the non-zero exit are the answer.

This is verification that can fail, which is the point. Most β€œwe have offsite backups” claims have never been tested this way, and a surprising number of append-only configurations turn out to have been applied to the wrong repository path or lost across a container recreation.

Object Lock (WORM)

Stronger, because it is enforced by the storage service rather than by an application on a host you also operate.

Configuration changeS3 Object Lock
BUCKET=example-app-backups

# Object Lock requires versioning; the flag enables both at creation time.
aws s3api create-bucket --bucket "$BUCKET" --region eu-west-1 --create-bucket-configuration LocationConstraint=eu-west-1 --object-lock-enabled-for-bucket

# Default retention: every object placed in the bucket is locked for 35 days.
aws s3api put-object-lock-configuration --bucket "$BUCKET" --object-lock-configuration '{
  "ObjectLockEnabled": "Enabled",
  "Rule": { "DefaultRetention": { "Mode": "GOVERNANCE", "Days": 35 } }
}'

# Confirm it applied
aws s3api get-object-lock-configuration --bucket "$BUCKET"

A pull-based backup

The strongest arrangement and the least used: the backup server initiates the connection and pulls, so the production host holds no backup credentials at all and does not know where the backups live.

The production host writes its dumps to a local staging directory. The backup host connects over SSH with a restricted key, reads the staging directory, and stores the result in a repository the production host cannot address. A compromise of production yields the staging directory β€” one night’s data β€” and nothing else.

The cost is a backup host that has read access to production, which is its own concentration of risk and needs the same care. It is the right shape when the data justifies it.

Two media, honestly

The β€œ2” in 3-2-1 predates cloud storage and gets treated as a formality. Its actual purpose is to avoid a correlated failure across copies, and correlation today comes from software far more than from hardware.

Failures that hit every copy in the same technology at once:

  • A bug in your backup tool that writes archives it cannot read. Same tool everywhere, same corruption everywhere.
  • A cloud provider region or account issue. Same provider for the β€œoffsite” copy and the primary means one billing dispute takes both.
  • A filesystem or storage-driver bug. Two ZFS pools on two hosts running the same kernel are one bug apart.

Which gives a practical reading of β€œ2 media”: make the second copy differ in the dimension most likely to fail. For most Docker stacks, the highest-value diversity is format, not hardware β€” a restic repository and a plain pg_dump file, because a plain SQL dump is readable by anything and depends on no tool being intact.

Retention that survives slow corruption

Deletion is the loud failure. The quiet one is corruption that started weeks ago and has been faithfully backed up ever since.

A retention policy of β€œkeep 7 daily” gives you a one-week window to notice. A table dropped by a bad migration five weeks ago, or a slow encryption run that began before anyone noticed, is beyond recovery β€” every retained snapshot contains the damage.

Data-loss riskrestic retention
export RESTIC_REPOSITORY='rest:http://backup-01.example.com:8000/app-01'
export RESTIC_PASSWORD_FILE=/etc/restic/password

# ALWAYS run without --prune first and read the list of what would go
restic forget --keep-daily 14 --keep-weekly 8 --keep-monthly 12 --keep-yearly 3 --host app-01 --dry-run

# Only then, with --prune to reclaim the space
restic forget --keep-daily 14 --keep-weekly 8 --keep-monthly 12 --keep-yearly 3 --host app-01 --prune

forget removes snapshots from the index; prune removes the data that no remaining snapshot references. Running forget alone is reversible in practice until a prune happens, which is why --dry-run before --prune is the habit worth having. Note also --host app-01 β€” without it, a policy expressed as β€œkeep 14 daily” is applied across every host’s snapshots collectively, which silently thins the machines that back up less often.

The shape above β€” a fortnight of dailies, two months of weeklies, a year of monthlies β€” costs very little with deduplication and moves your detection window from one week to one year. For most stacks the incremental storage is a few percent, because the monthly snapshots share almost all their blocks.

Knowledge check

Knowledge check Β· 5 questions

  1. Q1. A host has live data on a volume, a nightly tar in /backup on the same host, and a restic repository in S3 with credentials in /etc/restic. Why does this fail despite satisfying 3-2-1?

  2. Q2. What is the practical difference between S3 Object Lock GOVERNANCE and COMPLIANCE mode?

  3. Q3. Which of these are genuine tests of backup independence? Select all that apply.

  4. Q4. A simple `DELETE` (no version ID) against an Object-Locked S3 object returns 200 OK. What happened?

  5. Q5. Keeping seven daily snapshots is adequate retention as long as they are immutable and offsite.

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