In a container registry, what is the difference between referencing an image by tag (for example myapp:1.4) and referencing it by its sha256 digest (myapp@sha256:ab12...)?
answer
- tag = mutable pointer, digest = content address
- sha256 of the manifest bytes
- RepoDigest ≠ IMAGE ID
- tag for humans, digest for machines
- multi-arch: pin the index digest
basics
~20 sA tag is a mutable, human-friendly label the registry can repoint to different content at any time. A sha256 digest is the hash of the image manifest, so it is immutable and content-addressed: the same digest always resolves to exactly the same bytes.
solid answer
~50 sA **tag** is just a named pointer stored in the repository's tag namespace. Anyone with push rights can push different content under the same tag, so `myapp:1.4` today and `myapp:1.4` tomorrow may be different images — pulls are not reproducible. A **digest** is the SHA-256 hash of the image manifest document (which itself lists the config blob and layer digests). Because the name *is* the hash of the content, `myapp@sha256:ab12...` is immutable: if the registry returned different bytes, the hash would not verify and the client would reject it. That is also how the pull path gets integrity checking for free. Practically: tags are for humans and for expressing intent ("the 1.4 line"), digests are for machines and for reproducibility. Production deploys and `FROM` lines in a Dockerfile should resolve to a digest; CI decides *which* digest a tag currently means and records it.
code
bash · 9 linesdocker pull myapp:1.4
docker images --digests myapp
docker inspect --format '{{index .RepoDigests 0}}' myapp:1.4
# query the registry without pulling layers
docker buildx imagetools inspect myapp:1.4
# pull the exact bytes, tag-independent
docker pull myapp@sha256:ab12cd34ef56....go deeper
Be able to state plainly that tags move and digests do not, and show how to find a digest with docker images --digests.
Explain that the digest hashes the manifest, that changing config or any layer changes it, and that repo digest and image ID are different values.
Tie it to deployment practice: CI resolves tag to digest once, records it, and deploys the digest; discuss untagged-manifest garbage collection breaking old pins.
Frame it as a supply-chain boundary — digests give reproducibility and integrity, signatures give provenance, and policy should be about which digests are admitted, not which tags exist.
## Two different kinds of name A container image reference has three parts: a registry host, a repository path, and a *selector*. The selector is either a **tag** (`:1.4`) or a **digest** (`@sha256:ab12…`). They look similar in a command line but they are fundamentally different kinds of name. A **tag** is a mutable key in a mutable map. The registry keeps, per repository, a table from tag name to manifest digest. `docker push myapp:1.4` writes an entry in that table. Pushing again with the same tag overwrites the entry — the old manifest usually still exists (it is still addressable by its digest) but nothing named points at it any more. Nothing in the protocol prevents this; the registry API explicitly supports repointing tags. So a tag answers the question "what does this name mean *right now*?", and the answer can change between two pulls a minute apart. A **digest** is a content address. When you build an image, the registry stores a JSON **manifest** that lists the digest of the config blob and the digest of every layer blob. The image digest is `sha256` over the exact bytes of that manifest. Change any layer, any environment variable in the config, any label — the config blob changes, so the manifest changes, so the digest changes. Two references with the same digest are, byte for byte, the same image. There is no way to "push different content to a digest", because the digest is derived from the content. ## Why this matters operationally 1. **Reproducibility.** `FROM node:20-alpine` in a Dockerfile built in January and rebuilt in June can produce two different base layers. `FROM node:20-alpine@sha256:…` cannot. 2. **Integrity.** A client that pulls by digest verifies the hash of what it received. Pulling by tag verifies the layers against the manifest the registry handed you, but you have no independent statement about *which* manifest you should have gotten. Digest pinning is the base layer that image signing builds on. 3. **Debuggability.** "Which image is running in prod?" has one true answer only if you record a digest. Tag answers rot. 4. **Caching semantics.** Because a digest is immutable, a node that already has that digest never needs to re-check the registry. A tag always needs a round trip to see whether it moved. ## Finding the digest Locally, after a push or pull, `docker images --digests` shows the **repo digest** — the digest under which that image is known in that registry. It is empty for images that were only built locally and never pushed, because the digest is a property of the pushed manifest, not of the local build. `docker inspect --format '{{index .RepoDigests 0}}' myapp:1.4` extracts it. `docker buildx imagetools inspect myapp:1.4` queries the registry directly without pulling. Beware a common confusion: `docker images` also shows an **IMAGE ID**, which is the digest of the *local image config*, not the registry manifest digest. They are different hashes and will not match. ## Multi-architecture wrinkle For a multi-platform image, the tag points at an **image index** (also called a manifest list): a small document listing one manifest digest per platform. So `myapp:1.4` → index digest → per-platform manifest digest → layers. When you pin, prefer the *index* digest: it is still immutable, and it still lets an arm64 node and an amd64 node each pull the right variant. Pinning a per-platform manifest digest works too, but it hard-codes the architecture, which usually breaks a mixed fleet. ## The tradeoff Digests are unreadable and do not update themselves. A repository where every `FROM` is a bare digest tells a reviewer nothing about what version they are looking at, and nothing pulls in the security patch that the base-image maintainer shipped last week. The standard resolution is to write **both**: `FROM node:20.11.1-alpine@sha256:…`. The tag documents intent for humans, the digest is what actually resolves, and an automated dependency bot opens a pull request when the tag starts pointing at a newer digest — so the update becomes a reviewed, dated commit rather than a silent change under a running system. The mental model to keep: *tags are for people, digests are for machines, and the moment where a tag becomes a digest should be an event you record.*
- If a tag is repointed, is the old image gone from the registry?Not immediately. The old manifest and its layer blobs are still stored and still reachable by digest; only the tag entry moved. It becomes untagged and therefore a garbage-collection candidate, so most registries will delete it on the next GC run or per a retention policy. That means a digest pin can eventually break with a manifest-unknown error if the registry prunes untagged manifests.
- Why does an image I just built locally have no repo digest?The digest is the hash of the manifest as stored in a registry, and that manifest is produced during the push. Until you push (or pull) the image, there is no registry manifest and therefore no repo digest — `docker images --digests` shows `<none>`. The IMAGE ID you do see is the local image config hash, which is a different value.
A tag is like a Git branch name — it moves. A digest is like a commit SHA — it names exactly one snapshot forever.
saying these in an interview costs you the question
- Saying the IMAGE ID in `docker images` is the same as the sha256 digest used in `image@sha256:…`
- Believing a tag cannot be reused or that the registry prevents overwriting it
- Thinking pulling by tag gives the same image every time because layers are cached
- Assuming the digest is a hash of the layer tarballs rather than of the manifest document
- Claiming digest pinning makes signing unnecessary — it fixes the bytes but says nothing about who produced them