Chapter 2: Images
Topic 09

Tags vs Digests

PinningImmutability

A tag is a human-readable label; a digest is an immutable content identifier. The previous topic established the mechanism — a tag is a pointer the registry can move, a digest is the hash of fixed content. This topic is about the operational consequence, and it concentrates around one tag: :latest, which is not "the newest version" but simply the default tag, a pointer someone can move to anything at all, including older or broken content.

Getting this distinction right is what separates a deploy you can reproduce from one that mysteriously changes overnight. The split is not academic — it decides whether a redeploy or an autoscale event runs the same code as the rest of your fleet, and whether an incident review can say exactly what was running.

What :latest Actually Is

When you run docker pull postgres with no tag, Docker assumes :latest. That is the entire meaning of the tag — it is the default Docker fills in when you omit one. It carries no "newest" semantics: :latest is whatever was last pushed to that name, which may be a stale build, a broken build, or a different major version than you expect. A maintainer who pushes a 2.0 beta to :latest has made :latest the beta.

The trap is that the word reads like a promise the registry does not make. Plenty of projects deliberately do not move :latest to prereleases, and plenty do — there is no rule, only convention, and conventions vary per image. Treating :latest as a version is the single most common reproducibility mistake in Docker.

Tags Move, Digests Don't

Push driftwood/web:1.4.0 today and again next week with a rebuilt image, and the tag now resolves to the new content — the pointer moved. The old bytes still exist and still carry their own digest, but anyone who pulls driftwood/web:1.4.0 after the second push gets the rebuild. A digest, driftwood/web@sha256:…, can never resolve to anything other than the exact content it names, because the name is the content's hash. That is why the digest, not the tag, is the unit of reproducibility.

Two ways to name the same image, pulling in opposite directions
Tag
web:1.4.0 — a mutable, human-readable pointer. Anyone with push access can move it, so the same name can resolve to new bytes tomorrow.
Digest @sha256:…
An immutable content hash. The name is the hash of the bytes, so it always names the same image and can never be remapped.

A Tagging Strategy

The workable pattern uses both kinds of label for their strengths: immutable version tags that you never overwrite (1.4.0), plus moving convenience tags that intentionally track the newest in a series (1.4, 1, latest). A human reading 1.4 gets "the newest 1.4.x"; a human reading 1.4.0 gets exactly that release, forever. The rule that makes it hold together is simple: never re-push a released version tag. The full release strategy lands in the CI chapter; the discipline starts here.

Pinning in Practice

Two places deserve a digest. The base images in a Dockerfile — FROM postgres:16@sha256:… — so a rebuild months later gets byte-identical content rather than whatever 16 points at by then. And deployed images in production, so a redeploy or autoscale event runs exactly what you tested. CI is the natural place to resolve a tag to its digest at build time and bake the pinned reference into the artifact and the release record.

Pinning by digest looks verbose and feels like overkill until the first time a moving base tag silently changes a build's behavior — then it looks like the cheapest insurance you ever bought. You still write the readable tag in the Dockerfile as a comment; the digest is what the machine resolves.

The Reproducibility Payoff

The payoff arrives during an incident. When the question is "what exactly was running when this broke," a recorded digest answers it precisely — those bytes, that manifest, no ambiguity. A tag answers it only as "whatever 1.4.0 pointed at, assuming nobody moved it," which during a 3am postmortem is not an answer at all.

Pinning turns "it worked yesterday" from a shrug into a verifiable claim. You can pull the recorded digest, run it, and confirm the behavior — because the digest names the same bytes today that it named when the incident happened.

Tag vs Digest

Tag — a mutable, human-friendly pointer (driftwood/web:1.4.0, :latest) that anyone with push access can move to new content. Use it for readable versioning and for convenience handles that should track the newest in a series.

Digest — an immutable content hash (@sha256:…) that always names the same bytes and can never be remapped. Use it to pin exactly what runs in production and what a Dockerfile builds on. Tag for people, digest for reproducibility — and use both.

Common Mistakes
  • Running :latest in production and assuming it pins a version — a new push silently changes it, so a redeploy or autoscale event can pull different code than the rest of the fleet is already running.
  • Re-pushing an already-released version tag like 1.4.0 to "patch" it — everyone who pinned that tag now gets different bytes under the same name, which quietly breaks the immutability contract they relied on.
  • Building on FROM postgres:16 (a moving tag) and expecting reproducible builds — the base can change between two builds, so cut a digest pin if the build must reproduce.
  • Deploying by tag but never recording the resolved digest, so after an incident there is no precise record of what actually ran and "what shipped" becomes guesswork.
Best Practices
  • Pin production and base images by digest so neither a moved tag nor a rebuilt base can change what runs without an explicit code change you can see in review.
  • Treat released version tags as immutable — never re-push 1.4.0; cut 1.4.1 instead, so anyone pinned to the old tag keeps the bytes they tested.
  • Offer moving tags (latest, 1, 1.4) for human convenience, but never depend on them for anything that must be reproducible.
  • Capture and store the deployed image's digest in the release record so "what shipped" is always answerable exactly, especially during a postmortem.
Comparable tools Semantic versioning the tag conventions apply identically in Podman and any OCI registry skopeo · registry API resolve a tag to its digest without pulling npm · pip lockfiles the same mutable-tag-vs-locked-version problem, solved with a pin

Knowledge Check

What does the :latest tag actually mean?

  • It is the default tag Docker assumes when none is given, pointing at whatever was last pushed there
  • It always resolves, by design, to the single newest stable release of the image in the repository
  • It is an automatic, registry-maintained alias for the highest semantic-version tag currently available
  • It triggers the registry to build and assemble a completely fresh image on demand at pull time

Why are tags mutable while digests are not?

  • A tag is a pointer the registry can remap, while a digest is the hash of fixed content and can only name those bytes
  • Digests can be edited directly by registry administrators, whereas tags require a full re-push of the image to change
  • Tags are permanently locked to a fixed version number, while digests float along to track the newest build
  • Tags automatically expire after a set retention period, while digests are kept stored permanently in the registry

When should you reference an image by digest rather than by tag?

  • For production deploys and Dockerfile base images, where a redeploy or rebuild must get byte-identical content
  • For every casual one-off local command typed at the terminal, since a long digest is far easier to type and remember than a short tag
  • Only when you specifically need a faster download from the registry than a plain tag reference provides
  • Only for images in a private registry, since public images on Docker Hub cannot be pinned by digest

Why does re-pushing an already-released version tag like 1.4.0 break consumers?

  • Anyone who pinned 1.4.0 now pulls different bytes under the same name, breaking the immutability they relied on
  • The registry outright rejects the second push as a conflict, so the new build never reaches any consumer at all
  • The original image's digest is permanently deleted from the registry, so every prior pull stops working entirely
  • Every machine that ever ran 1.4.0 is automatically force-updated to the new image immediately and without warning

You got correct