Skip to main content
RunBook Academy

Docker & ContainersXIII Β· RegistriesDigests

Immutable digests β€” why tags are not enough

Foundation⏱ ~22 mindocker

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

Not yet marked complete on this device.

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-stopped container 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 -d for the whole file.
  • An autoscaler adding a node.
  • An operator running docker compose pull && docker compose up -d because 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

Read-only / Safefleet digest census
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...02af

Illustrative 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:

Read-only / Safeaggregated across twelve hosts
$ 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...02af

Illustrative 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.

Read-only / Safeindex digest and platform digests
docker buildx imagetools inspect docker.io/library/alpine:latest
Name:      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/v6

Pin 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.

Read-only / Saferesolve the current digest for a set of tags
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"
done
docker.io/library/nginx:1.27 -> docker.io/library/nginx@sha256:0000...0000
docker.io/library/postgres:16 -> docker.io/library/postgres@sha256:1111...1111

Illustrative 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

  1. Q1. Someone re-pushes a different image to the tag `api:1.4.0`, which is deployed on twelve hosts. What happens?

  2. 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?

  3. Q3. Digest pinning protects you against which of the following? Select all that apply.

  4. Q4. Setting `pull_policy: always` in a Compose file makes the service start from the local cache when the registry is unreachable.

  5. 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.