Docker & ContainersXXI Β· BackupOffsite
Offsite copies and the 3-2-1 rule
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
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.
| Copy | Where | Destroyed by |
|---|---|---|
| Live data | app_pgdata volume on host-01 | Host loss, docker volume rm, ransomware on host-01 |
| Nightly tar | /backup on host-01 | Host loss, disk failure, ransomware on host-01 |
| restic repository | S3, credentials in /etc/restic on host-01 | Ransomware 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.
# 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 /dataexport 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$ restic forget --keep-last 1 --pruneApplying 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 serverIllustrative 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.
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.
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 --pruneforget 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
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?
Q2. What is the practical difference between S3 Object Lock GOVERNANCE and COMPLIANCE mode?
Q3. Which of these are genuine tests of backup independence? Select all that apply.
Q4. A simple `DELETE` (no version ID) against an Object-Locked S3 object returns 200 OK. What happened?
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.