Docker & ContainersXIII Β· RegistriesBackup and restore
Registry backup and restore
What you'll learn
- Enumerate everything a registry backup must contain for a restore to produce working pulls
- Take a consistent backup of a live registry, or know why yours is not consistent
- Restore and verify with a test whose failure is unambiguous
- Explain why replication is not a backup
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 registry is the source of every production image. If it is lost, you cannot deploy and β worse β you cannot roll back. Treat it like the database it is.
Treating it like a database means being specific about what its state consists of, because the intuitive answer is wrong in a way that produces a backup which restores cleanly and then serves nothing.
What a backup must contain
A Distribution registryβs state is two trees and they are not interchangeable.
The blob store. Every layer, every image config, every manifest
body, stored once each under a path derived from its own SHA-256. This
is where essentially all the bytes are. It is also completely
anonymous: nothing in it records that a particular blob is a layer of
myapp:release-2026-08.
The naming tree. Under repositories/<name>/ sit the tag links
(_manifests/tags/<tag>/current/link, a file whose contents are a
digest), the manifest revision history, and the per-repository links to
the blobs each repository references.
Beyond storage, a restore also needs:
- The registry configuration.
delete.enabled, the storage driver settings, the auth configuration, the HTTP secret. A restore against a default config produces a registry that refuses deletions and possibly rejects your clients. - TLS material, or a documented path to reissuing it.
- Authentication state, wherever it lives β an htpasswd file, a token service configuration, or an external IdP registration.
- For Harbor, Quay or GitLab: the product database, restored to a point consistent with the storage. Harborβs own guidance covers the database volume and the registry storage together; check your versionβs documentation for what else its data volume holds, because a database restored against freshly generated instance secrets can leave stored credentials unreadable.
The consistency problem
A tar of a live registry is not a snapshot. It reads the tree over
minutes while clients are pushing, and the result depends on what
happened during that window.
Cold backup
STAMP=$(date +%F)
docker stop registry
tar czf "/backup/registry-${STAMP}.tar.gz" -C /srv/registry .
docker start registryPulls fail for the duration, which is usually acceptable overnight and never acceptable during business hours.
Read-only backup
Better: put the registry in maintenance read-only rather than stopping it. Pulls keep working β so deploys of existing images are unaffected β and no new content can arrive to tear the capture.
STAMP=$(date +%F)
docker rm -f registry
docker run -d --name registry --restart always -p 5000:5000 \
-e REGISTRY_STORAGE_MAINTENANCE_READONLY_ENABLED=true \
-v /srv/registry:/var/lib/registry registry:3
tar czf "/backup/registry-${STAMP}.tar.gz" -C /srv/registry .
docker rm -f registry
docker run -d --name registry --restart always -p 5000:5000 \
-v /srv/registry:/var/lib/registry registry:3Snapshot backup
Best, where the storage supports it: an atomic snapshot, taken in milliseconds, with the archive built from the snapshot afterwards while the registry serves normally.
STAMP=$(date +%F)
sudo lvcreate --size 5G --snapshot --name registry-snap /dev/vg0/registry
sudo mkdir -p /mnt/snap
sudo mount -o ro /dev/vg0/registry-snap /mnt/snap
sudo tar czf "/backup/registry-${STAMP}.tar.gz" -C /mnt/snap .
sudo umount /mnt/snap
sudo lvremove -f /dev/vg0/registry-snapSize the snapshot for the writes expected during the capture, not for the size of the registry. A snapshot that fills is dropped by the kernel and the backup silently becomes garbage.
Harbor and friends
STAMP=$(date +%F)
docker exec harbor-db pg_dump -U postgres registry > "/backup/harbor-db-${STAMP}.sql"The registry storage and the database describe the same objects from two sides. Restoring a database from Tuesday against storage from Wednesday gives you a Harbor that lists artefacts it cannot serve and serves artefacts it does not list.
Restore
- Record a known digest before you start β one you can pull today β so the restore has something falsifiable to prove.
- Verify the archive reads end to end without extracting it.
- Extract to a new directory, not over the live one.
- Start a registry against the restored directory on a different port, leaving the original untouched.
- Pull the recorded digest from the restored registry, with no local copy present, so blobs are actually transferred.
- Compare the tag inventory against what you expect.
- Only then cut traffic over.
ARCHIVE=/backup/registry-2026-08-08.tar.gz
gzip -t "$ARCHIVE" && echo 'gzip stream OK'
tar tzf "$ARCHIVE" > /dev/null && echo 'archive readable end to end'
tar tzf "$ARCHIVE" | grep -c 'docker/registry/v2/repositories/'gzip stream OK
archive readable end to end
1483Illustrative output
That last count is the check people miss. A nonzero number of entries
under repositories/ is the evidence that the naming tree is in the
archive at all β the difference between a backup and a pile of
anonymous blobs.
ARCHIVE=/backup/registry-2026-08-08.tar.gz
RESTORE_DIR=/srv/registry-restore
sudo mkdir -p "$RESTORE_DIR"
sudo tar xzf "$ARCHIVE" -C "$RESTORE_DIR"
docker run -d --name registry-restore -p 5001:5000 \
-v "$RESTORE_DIR":/var/lib/registry registry:3EXPECTED_DIGEST=sha256:REPLACE_ME
REF="localhost:5001/myorg/myapp@${EXPECTED_DIGEST}"
docker image rm "$REF" 2>/dev/null || true
if docker pull "$REF"; then
echo 'RESTORE OK: manifest resolved and all blobs transferred'
else
echo 'RESTORE FAILED: do not cut over' >&2
fiRESTORE OK: manifest resolved and all blobs transferredIllustrative output
Pulling by digest is what makes this a real test. Content addressing means the daemon verifies the bytes it received against the digest it asked for, so a successful pull is a cryptographic statement that the restored blobs are the original blobs β not merely that a file of the right size was present.
RESTORED=http://localhost:5001
curl -fsS "$RESTORED/v2/_catalog?n=1000" \
| tr ',' '\n' | grep -c '"' || true
for repo in myorg/myapp myorg/myapi; do
printf '%s: ' "$repo"
curl -fsS "$RESTORED/v2/${repo}/tags/list" | tr ',' '\n' | grep -c '"' || true
done42
myorg/myapp: 11
myorg/myapi: 9Illustrative output
RPO in a registry is not RPO in a database
A 24-hour-old registry backup means up to 24 hours of pushed images are missing. That sounds like the database framing, and it is not quite, because most of those images are reproducible: the source is in version control, the pipeline is in version control, and rebuilding regenerates them.
The images that are genuinely lost are the ones you cannot rebuild:
- built from a commit that has since been force-pushed away,
- built with a dependency version that is no longer resolvable,
- built by a pipeline whose configuration has since changed,
- built from inputs that were never in version control at all.
So the useful question is not βhow many hours of images can we loseβ but βhow many of the images on our current deploy path can we reproduce byte-for-byte todayβ. For most organisations the honest answer is fewer than they assume, and finding that out during a restore test is much cheaper than finding it out during a recovery.
Knowledge check
Knowledge check Β· 5 questions
Q1. A backup captures only the blob store of a Distribution registry. What does a restore from it produce?
Q2. You cannot take an atomic snapshot and must copy the two trees separately from a live registry. Which order is safer, and why?
Q3. Which of these belong in a registry restore plan beyond the storage data? Select all that apply.
Q4. S3 cross-region replication of the registry bucket is an adequate substitute for backups.
Q5. Verifying a restore with a full `docker pull` by digest proves the restored blobs are byte-identical to the originals.
Passing score: 75%. Answers are checked in this browser.