Skip to main content
DevOps Insightsguides & answers

Docker · commonly confused

Docker image tag vs digest

Short answer

A tag is a name the publisher can move to a different image at any time; a digest is a hash of the image's content, so it can only ever mean that one image. Deploy by tag and two pulls of the same reference can give you different code. Pin by digest and you get the same image every time — including after a security fix has been published for it.

First published

On this page · 10 sections

In simple terms

Both come after the image name and both look like a version, which is why they get treated as one thing. alpine:3.21 and alpine@sha256:… are not two spellings of the same reference. One is a label; the other is a fingerprint.

A tag is a pointer the publisher controls. Docker's documentation says plainly that tags are mutable: alpine:3.21 might mean 3.21.1 today and 3.21.4 three months from now, because the publisher moved the tag when they shipped a patch. For a base image that is often what you want — the tag follows the fixes.

A digest is computed from the image's content. A different image has a different digest, so a digest cannot be moved to point somewhere else; it only ever names the image it was computed from.

So the choice is between two failure modes rather than between right and wrong. A tag can change underneath you. A digest cannot change even when you need it to — pin one and updates, security fixes included, arrive only when somebody changes the digest on purpose.

Words used on this page

image reference
The full name you pull or run: registry and repository, then a tag after :, a digest after @, or both. With neither, Docker and Kubernetes use the tag latest.
tag
A human-readable name for an image, such as 3.21. The publisher can point it at a different image at any time, unless the registry makes tags immutable.
digest
An identifier computed from the image's content, such as sha256:2e86…. A different image has a different digest, so a digest always names the same image.
pinning
Referring to an image by digest, so every build or deploy uses exactly that image until someone changes the digest.
immutable tags
A Docker Hub repository setting that stops a pushed tag from being overwritten or deleted. By default all tags on Docker Hub are mutable.
imagePullPolicy
When the kubelet pulls: Always, IfNotPresent or Never. If you leave it out, a digest or a tag other than latest gets IfNotPresent, and :latest or no tag gets Always.
imageID
In a pod's container status, the image the container runtime actually resolved. It can differ from the image string in the pod spec.
registry
The server that stores images and resolves a tag to a digest when you pull, such as Docker Hub at docker.io.

What actually happens

An image reference is a repository plus a tag (:3.21), a digest (@sha256: and 64 hex characters), or both. A tag is resolved at the moment of the pull: the Kubernetes documentation describes the runtime contacting the registry, resolving the image tag or name to a digest, and downloading any layers not already cached. Every pull by tag is really a pull by whatever digest the tag pointed to at that moment.

Docker shows that resolution if you look. docker pull ubuntu:24.04 prints a Digest: sha256:… line near the end of its output, and docker images --digests lists the digest beside each tag. Docker's documentation calls the digest a content-addressable identifier, predictable as long as the input used to generate the image is unchanged, and accepts it in place of a tag in pull, create, run, rmi and a Dockerfile's FROM.

Kubernetes adds the part that turns a moved tag into an incident. With imagePullPolicy omitted, a tag other than :latest defaults to IfNotPresent, which pulls only if the image is not already on the node; :latest or no tag defaults to Always, and a digest to IfNotPresent. A node that already has myapp:1.4 keeps what it pulled, and a new node pulls whatever myapp:1.4 means today. The documentation states the outcome: if the registry changes what a tag represents, you might end up with a mix of Pods running the old and new code. A digest cannot resolve to anything else, so that mix cannot happen.

Writing both — alpine:3.21@sha256:… — is the form Docker's own documentation uses, and for a name with tag and digest the Kubernetes documentation says only the digest is used for pulling. So in Kubernetes the tag beside a digest chooses nothing. It is a label for whoever reads the manifest, and keeping it accurate after an update is your job.

What a tag resolved to, and how to pin it

# A tag is resolved at pull time. The Digest line is what it means today.
docker pull ubuntu:24.04
#   Digest: sha256:<digest>

# A digest is the same image on every machine, on every day.
docker pull ubuntu@sha256:<digest>

# Which digest each local tag currently points at.
docker images --digests

# Dockerfile: the tag for the reader, the digest for the build.
FROM alpine:3.21@sha256:<digest>

# Kubernetes: what each pod's runtime actually resolved, per node.
kubectl get pods -l app=myapp -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.nodeName}{"\t"}{.status.containerStatuses[0].imageID}{"\n"}{end}'

In the last command, the same spec with two different image IDs is what a moved tag looks like under IfNotPresent. The API documents imageID as possibly differing from the image in the pod spec because the runtime resolved it, which is exactly the gap a tag leaves.

Try it yourself

Before you start

  • Docker, with network access to the registry. For a private registry, run docker login <registry> first.
  • The digest you want to pin. It is printed on the Digest: line of docker pull and docker push, and docker images --digests lists it for pulled images. It looks like sha256: followed by 64 hexadecimal characters.
  • For Kubernetes, kubectl access to the namespace, so you can read the image each pod actually runs from its container status.
  • A way to move pins forward, before you start pinning: for example GitHub Dependabot with package-ecosystem: "docker", which opens pull requests that update tags and digests in your Dockerfiles.

Pulling by tag, then by the digest it printed

Quoted from the Pull an image by digest example in Docker's docker image pull reference, not reproduced: Docker is not installed on the machine this page was checked on, and the reference does not say when the output was captured or with which Docker version. The digest is whatever ubuntu:24.04 pointed to then; pulling the same tag today can print a different one.

Command

docker pull ubuntu:24.04
docker pull ubuntu@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30

Output · as shown in Docker docs: docker image pull, pull an image by digest

$ docker pull ubuntu:24.04

24.04: Pulling from library/ubuntu
125a6e411906: Pull complete
Digest: sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30
Status: Downloaded newer image for ubuntu:24.04
docker.io/library/ubuntu:24.04

$ docker pull ubuntu@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30

docker.io/library/ubuntu@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30: Pulling from library/ubuntu
Digest: sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30
Status: Image is up to date for ubuntu@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30
docker.io/library/ubuntu@sha256:2e863c44b718727c860746568e1d54afd13b2fa71b160f5cd9058fc436217b30

How to read it

  • The tag pull printed Digest: sha256:2e86…: the image ubuntu:24.04 pointed to at that moment. It is not the last line; Status: and the full reference follow it.
  • The last line, docker.io/library/ubuntu:24.04, shows what the short name meant: the ubuntu repository under library on Docker Hub.
  • The digest pull reports Image is up to date and has no Pull complete lines: the digest names exactly the image the tag pull had just fetched, so there was nothing to download.
  • The digest pull gives the same image on every machine on every day; the tag pull gives whatever the tag points to when you run it. Docker's note on this example states the cost: with a pinned digest, Docker does not pull updated images, security updates included, until you change the digest.

The mistake this causes

Deploying a mutable tag — myapp:1.4, or :latest — and treating it as if it named one fixed image.

Why people do it A tag is resolved when it is pulled, not when it is written. Under IfNotPresent each node resolves it once, whenever it first needed the image, so one manifest can mean different images on different nodes. Rolling back to a tag that has since moved does not roll back to the code you remember — the Kubernetes documentation gives harder rollbacks as a reason to avoid :latest in production.

What you see Pods from one Deployment behave differently, often only on some nodes. A fix appears on new nodes and not old ones. A rebuild of an unchanged Dockerfile produces a different image because its FROM tag moved. The pod spec shows the same image everywhere; only each pod's resolved image ID differs.

If it goes wrong

Pods from one Deployment behave differently, though every pod spec shows the same image
Compare each pod's status.containerStatuses[].imageID. Different values mean the tag moved between pulls on different nodes. Deploy by digest (name@sha256:...) so every node runs the same image.
You changed a Deployment's image to :latest, and nodes keep running the cached image
imagePullPolicy is filled in once, when the object is created, and is not updated when the tag changes later. Set imagePullPolicy: Always yourself, or better, deploy a digest.
Rebuilding an unchanged Dockerfile produced an image that behaves differently
Its FROM tag now resolves to a different image. Pin FROM name:tag@sha256:..., and update the digest on purpose, for example through Dependabot pull requests.
Docker Hub refuses a push to a tag that already exists
The repository has immutable tags turned on, so existing tags cannot be overwritten. Push the new image under a new tag; that is what the setting is for.
A manifest says name:1.4@sha256:..., but the pods run something other than 1.4
With both a tag and a digest, Kubernetes pulls by the digest only, and the tag is just a label. Look the digest up again and update the tag and digest together.

How it shows up in production

A team tags releases myapp:<version>, and for a small hotfix re-pushes the same tag rather than bumping the version.

The Deployment uses that tag with the default pull policy, IfNotPresent. Nodes that already ran the release keep their cached image. A node added to the cluster that afternoon pulls the re-pushed one.

Requests now succeed or fail depending on which pod they land on. Every manifest and every kubectl get says the same version, so the first hours go into the application rather than the image.

The opposite mistake looks just as calm. A base image pinned by digest and never updated keeps every vulnerability it had on the day it was pinned. Docker's documentation names this trade-off — pinning opts you out of automated security fixes — and pairs pinning with an updater, suggesting GitHub Dependabot's docker ecosystem, so the pin moves by reviewed pull request instead of never.

How to tell which one you are hitting

Is this a Dockerfile FROM line, or what production runs?
For FROM, a tag picks up upstream patches on rebuild and a digest makes the build reproducible but needs an updater. For production, a digest — or a tag your team never re-pushes — is the only way to know every replica runs the same image.
Do pods with the same image string behave differently?
Compare each pod's status.containerStatuses[].imageID. If they differ, the tag moved between pulls, and the code is not where the problem is.
Did rebuilding an unchanged Dockerfile change behaviour?
Check whether its FROM tag now resolves to a different digest. Docker's own example: the same alpine:3.21 can be 3.21.1 one day and 3.21.4 three months later.
Will anything update your digest pins?
If not, they are frozen, against security fixes too. That is the cost Docker's documentation names; an updater is how you keep reproducibility without the staleness.

Security considerations

  • Whoever can push to a repository can move its tags, including to a malicious image if their credentials leak. Pinning by digest means a moved tag cannot change what you build or deploy, which is why Docker presents digest pinning as the way to fully secure supply chain integrity.
  • A digest pin never picks up security fixes by itself: Docker's documentation says pinning opts you out of automated security fixes. Pair every pin with an updater, such as Dependabot, or a check like Docker Scout's No outdated base images policy, which flags pins whose image has been updated.
  • For images you publish, turn on immutable tags for release tags on Docker Hub, so a version that passed testing cannot be overwritten later. Docker Hub tags are mutable by default.
  • In Kubernetes, :latest or no tag defaults to imagePullPolicy: Always, so every container start runs whatever the tag points to at that moment. Kubernetes advises against :latest in production because it is harder to track what is running and to roll back.

In an interview

How this gets asked in an interview

Usually phrased as What is the difference between an image tag and a digest, and which should production use?

What a strong answer contains A tag is a mutable pointer the publisher can move; a digest is a hash of the image's content and cannot change. Production should run by digest, or by tags that are never re-pushed, because a tag is resolved at pull time and under Kubernetes' default IfNotPresent each node resolves it once — so a moved tag produces a mix of old and new pods. The answer that shows experience names the cost as well: a digest pin never picks up a security fix on its own, so it needs an updater, and with name:tag@sha256:… Kubernetes pulls by the digest alone, so the tag is only a label.

The follow-up "Two pods from one Deployment run different code. How do you prove it is the image?" Compare the resolved imageID in each pod's container status. The same spec with a different imageID means the tag moved between pulls.