Docker & ContainersXIII Β· RegistriesDigests
Immutable digests β why tags are not enough
What you'll learn
- Explain the two-step resolution from tag to manifest to blobs, and where mutability enters
- Pull and deploy by digest, including for multi-platform images
- Detect a fleet running two different images under one tag
- Choose deliberately between an index digest and a platform manifest digest
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
Tags are mutable. Digests are not. This is the most important property of a Docker registry, and the one whose consequences are least often thought through.
docker pull nginx:1.27
# Whatever image the publisher has assigned to 1.27, right now
docker pull nginx@sha256:0000000000000000000000000000000000000000000000000000000000000000
# Always the exact image whose manifest hashes to that value
The interesting question is not βwhich is saferβ β everyone knows the answer. It is βwhat does it actually look like when a tag moves under a running systemβ, because the answer is not an error message.
What a moved tag does to a running fleet
Suppose registry.example.com/api:1.4.0 is deployed to twelve hosts.
Somebody rebuilds and re-pushes 1.4.0 β a hotfix, a CI re-run, a
latest-style convenience tag on a release branch.
Nothing happens. That is the problem.
Running containers do not consult the registry. Each of the twelve hosts keeps running the image it already has, because a container is bound to an image ID at create time, not to a tag. The new image arrives one host at a time, whenever something causes that host to re-create the container:
- A
restart: unless-stoppedcontainer after a host reboot β the container is re-created only if it was removed, so often not. - A deploy of an unrelated service that runs
docker compose up -dfor the whole file. - An autoscaler adding a node.
- An operator running
docker compose pull && docker compose up -dbecause a log looked odd.
So over the following week the fleet drifts into two populations. Some
hosts serve the old bytes, some the new, both reporting api:1.4.0 in
every dashboard, every docker ps, and every log line.
The one command that finds it
docker ps --format '{{.Names}}' | while read -r c; do
docker inspect --format '{{.Name}} {{.Config.Image}} {{.Image}}' "$c"
done/api registry.example.com/api:1.4.0 sha256:4f2a...c19d
/web registry.example.com/web:1.4.0 sha256:9b71...02afIllustrative output
.Config.Image is the reference as written at create time β the tag.
.Image is the image ID the container is actually running. Collect the
pair from every host and group by tag:
$ sort fleet-census.txt | awk '{print $2, $3}' | sort | uniq -c 7 registry.example.com/api:1.4.0 sha256:4f2a...c19d
5 registry.example.com/api:1.4.0 sha256:81e0...77b3
12 registry.example.com/web:1.4.0 sha256:9b71...02afIllustrative output
Seven hosts on one image, five on another, one tag. If you take one check away from this lesson, take this one: the count of distinct image IDs per deployed tag should be exactly one, and it is trivial to assert.
Finding the digest to pin
There are two digests for a multi-platform image and picking the wrong one is the usual mistake.
docker buildx imagetools inspect docker.io/library/alpine:latestName: docker.io/library/alpine:latest
MediaType: application/vnd.docker.distribution.manifest.list.v2+json
Digest: sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300
Manifests:
Name: docker.io/library/alpine:latest@sha256:e7d88de73db3d3fd9b2d63aa7f447a10fd0220b7cbf39803c803f2af9ba256b3
MediaType: application/vnd.docker.distribution.manifest.v2+json
Platform: linux/amd64
Name: docker.io/library/alpine:latest@sha256:e047bc2af17934d38c5a7fa9f46d443f1de3a7675546402592ef805cfa929f9d
MediaType: application/vnd.docker.distribution.manifest.v2+json
Platform: linux/arm/v6Pin the index digest β the Digest: at the top β when you want one
reference that works on every architecture in your fleet. Pin a
platform manifest digest only when you deliberately want to force
one architecture, and be aware that the reference will then fail to run
on any other.
Pinning in Compose
services:
api:
# api:1.4.0 β keep the human-readable version in a comment
image: registry.example.com/api@sha256:4f2ac19d0000000000000000000000000000000000000000000000000000c19d
labels:
org.opencontainers.image.version: "1.4.0"
Keeping pins current
The objection to digest pinning is real: an unreadable 64-hex string that no human will ever update means you stop getting security rebuilds.
The answer is to make bumping the pin a routine automated change rather than a manual one. Whatever tool you use, the shape is the same: resolve the tag to a digest, open a change, let CI verify it, merge.
for ref in \
docker.io/library/nginx:1.27 \
docker.io/library/postgres:16 \
; do
DIGEST=$(docker buildx imagetools inspect "$ref" \
| awk '/^Digest:/ {print $2; exit}')
printf '%s -> %s@%s\n' "$ref" "${ref%%:*}" "$DIGEST"
donedocker.io/library/nginx:1.27 -> docker.io/library/nginx@sha256:0000...0000
docker.io/library/postgres:16 -> docker.io/library/postgres@sha256:1111...1111Illustrative output
The point of routing it through review is that the digest bump becomes the artefact you can point at afterwards. βWhich base image change introduced thisβ is answerable from your own history instead of from the publisherβs.
Knowledge check
Knowledge check Β· 5 questions
Q1. Someone re-pushes a different image to the tag `api:1.4.0`, which is deployed on twelve hosts. What happens?
Q2. You inspect a multi-platform image with `docker buildx imagetools inspect`. Which digest should you pin so the reference works on both amd64 and arm64 hosts?
Q3. Digest pinning protects you against which of the following? Select all that apply.
Q4. Setting `pull_policy: always` in a Compose file makes the service start from the local cache when the registry is unreachable.
Q5. Grouping running containers by tag and counting distinct image IDs is enough to detect a fleet split by a moved tag.
Passing score: 75%. Answers are checked in this browser.