OnCallReady

Lesson 10.5 · Images & Builds · 18 min read

Images, tags and digests

In plain words

Think of a library book. The sticker on the spine says 'Harry Potter, newest copy', and the librarian can move that sticker to a different book whenever a new edition arrives. But every book also has an ISBN printed inside, and that number only ever means those exact pages.

An image tag like orders:2.14 or latest is the sticker: a pointer in the registry that anyone with push rights can move. The digest, sha256:..., is the ISBN: a hash of the content, so orders@sha256:aaa... can only be those bytes. latest is not 'newest', it is just the sticker you get when you did not ask for one. docker images, docker pull and docker inspect let you read both.

Why this matters

Two copies of the same app, started from "the same" image name, behave differently. Nothing in your config changed. The cause is almost always that an image name is a movable label, not the image itself. This lesson teaches you to read image names and to pin the one thing that cannot move.

What you need to know already: the previous lesson (layers and config), JSON (Ch 7), hashes like sha256 in passing (sha256sum prints a fingerprint of a file's content: same content, same hash).

Where images live: registries

Images are stored in a registry, a web server for images. You pull (download) images from it and push (upload) yours to it. Docker Hub (docker.io) is the public default; companies run private ones.

Anatomy of an image reference

registry.lab/team/orders:2.14.1@sha256:5f2c...
└── registry ┘└ repository ┘└ tag ┘└─ digest ─┘

The registry part is recognised by a dot, a colon (a port, Ch 9) or localhost in the first path component. That is why myteam/app is on Docker Hub but registry.lab/app is not.

Pulling, and reading what came down

$ docker pull nginx:1.27
1.27: Pulling from library/nginx
7a1c2f9b0d34: Pull complete
e0f3a1b2c4d5: Pull complete
Digest: sha256:e53039b8572d5bf098282edf0ae574e9a99a3cafd82d7f5b5fd7a3c4e91caf66
Status: Downloaded newer image for nginx:1.27
docker.io/library/nginx:1.27

One Pull complete line per layer. Digest: is the image's digest. Pull it again and nothing downloads:

$ docker pull nginx:1.27
1.27: Pulling from library/nginx
Digest: sha256:8f2c7d1e...
Status: Image is up to date for nginx:1.27

docker images lists what is on this machine:

$ docker images
REPOSITORY                          TAG                      IMAGE ID       CREATED       SIZE
maven                               3.9-eclipse-temurin-21   38e31ad7d198   3 weeks ago   558MB
eclipse-temurin                     21-jdk                   c3dd73f22330   3 weeks ago   489MB
eclipse-temurin                     21-jre                   6edd73fb109a   3 weeks ago   271MB
node                                22                       a2c07ee9c2a3   3 weeks ago   1.1GB
node                                22-slim                  085ddfb5e4ee   3 weeks ago   226MB
alpine                              3.20                     5f53374e5deb   3 weeks ago   8.3MB

(These are real images you will use: eclipse-temurin is Java, maven is a Java build tool, node is Node.js - the lessons explain each when you need it.) Read the columns carefully:

--digests adds the digest column:

$ docker images --digests nginx
REPOSITORY   TAG    DIGEST                                                                    IMAGE ID       CREATED       SIZE
nginx        1.27   sha256:e53039b8572d5bf098282edf0ae574e9a99a3cafd82d7f5b5fd7a3c4e91caf66   3b1e8a9f0c2d   3 weeks ago   192MB

Tags are mutable, digests are not

A tag is a pointer in the registry. Anyone allowed to push can move it:

Monday     orders:2.14  ->  sha256:aaa...
Tuesday    the build server pushes a hotfix as orders:2.14 again
           orders:2.14  ->  sha256:bbb...

A server that pulled on Monday runs aaa; one that pulls on Tuesday runs bbb. Same tag, different bytes, and nothing in your deploy config changed. A digest is the hash of the content itself: orders@sha256:aaa... can only ever mean those exact bytes. Change one byte and the hash changes.

$ docker inspect -f '{{index .RepoDigests 0}}' nginx:1.27
nginx@sha256:e53039b8572d5bf098282edf0ae574e9a99a3cafd82d7f5b5fd7a3c4e91caf66
$ docker pull nginx@sha256:e53039b8572d5bf098282edf0ae574e9a99a3cafd82d7f5b5fd7a3c4e91caf66

{{index .RepoDigests 0}} means "the first entry of the RepoDigests list" - the name@digest form you can pull or pin.

Why :latest is a production incident waiting to happen

latest is not special. It does not mean "newest"; it means "the tag you get when you did not type one". Whatever was pushed last with no tag, or pushed as latest on purpose, is latest.

What goes wrong in practice:

The rule: humans use tags, deployments use tags that never move (2.14.1, or the git commit id), and anything security-critical pins the digest. The registry lesson at the end of this chapter builds a real tagging strategy.

Pinning a base image in a Dockerfile

A Dockerfile is the recipe for building an image (next lesson). Its first line names the image you build on top of, the base image:

FROM eclipse-temurin:21-jre@sha256:0c7f714bf6df7d9c70526654c35b47...

The tag is kept for humans; the digest is what the builder uses. Now a rebuild next month produces the same base, byte for byte. Tools like Renovate or Dependabot (bots that watch your dependencies and open a pull request when a new version appears) can propose the new digest, so you still get security fixes, deliberately.

The cost is real: a pinned digest never picks up security fixes by itself. Pin and automate the bump.

inspect: the whole config

$ docker image inspect nginx:1.27 --format '{{json .Config}}'
{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","NGINX_VERSION=1.27.2"],"Entrypoint":["/docker-entrypoint.sh"],"Cmd":["nginx","-g","daemon off;"],"User":"","WorkingDir":"","ExposedPorts":{"80/tcp":{}},"Labels":{},"Volumes":null,"StopSignal":"SIGQUIT"}

--format is the long spelling of -f; {{json .Config}} prints the config section as JSON. That is everything a container of this image starts with: the entrypoint and cmd (together, the program to run - a later lesson), the user (empty = root), the environment, the ports, the stop signal. The templates worth memorising:

{{.Config.User}}                          who it runs as ("" = root)
{{json .Config.Entrypoint}}               the executable, as JSON
{{.Architecture}}                         arm64 / amd64
{{index .RepoDigests 0}}                  the digest to pin
{{range .RootFS.Layers}}{{println .}}{{end}}   one layer per line
{{.Size}}                                 bytes

.Architecture deserves a habit: on an Apple Silicon Mac or this arm64 VM, check it before you push anything that will run on amd64 servers.

What you can now do

Why it helps

The 3am version: two servers running the payments image behave differently and nothing in the deploy config changed. Someone re-pushed the tag, one server pulled on Monday and one on Tuesday. With digests you can prove it in one command. The PR version: a deploy config uses orders:latest, and you can explain the rollback that redeploys the broken build and the routine restart that silently upgrades prod on a server set to pull on every start. The security version: pinning a base image by digest makes rebuilds reproducible, but it also freezes out patches, so you ask for Renovate or Dependabot alongside it. And the Apple Silicon version: {{.Architecture}} on an image built on your Mac says arm64, before it reaches an amd64 server and dies.

Commands in this lesson

docker

FAQ

Is latest the newest version of an image?

Not necessarily. latest is only the default tag when you do not type one. It points at whatever was last pushed as latest, which may be old, may be a test build, or may not exist at all. Many projects never update it. Treat it as 'unknown' and always use an explicit version.

What is the difference between IMAGE ID and the digest?

In the classic Docker image store, IMAGE ID is the hash of the image config, a local identifier, while the digest in RepoDigests is the hash of the manifest in the registry. Pin and deploy by digest. Two tags with the same IMAGE ID are the same image. Note that with the newer containerd image store in Docker, IMAGE ID shows the manifest or index digest instead.

Why is SIZE in docker images bigger than what the registry shows?

docker images shows the uncompressed size on disk. Registries store and transfer compressed layers, often a third of that. Also, shared base layers are counted in every image's SIZE but stored only once, so adding up the column overstates your disk use. docker system df -v shows shared versus unique sizes.

If I pin by digest, how do I still get security patches?

You don't, by default, and that is the trade-off. A pinned base stays exactly as it was. The standard answer is automation: Renovate or Dependabot watches the tag, and when a new digest appears it opens a PR that bumps the pin. You get reproducible builds and patches, but patches arrive as reviewed changes instead of silently.

How does Docker know whether myteam/app is on Docker Hub or a private registry?

It looks at the first path component. If it contains a dot, a colon (port) or is localhost, it is a registry host: registry.lab/app. Otherwise the image is on Docker Hub, and single names get the library/ namespace: nginx means docker.io/library/nginx:latest. That is why you must tag with the full registry host before pushing to a private registry.

In an interview Junior

Why is using the latest tag in production a bad idea?

A tag is a movable pointer in the registry, and latest is not special: it is just the tag you get when you did not type one. It does not mean "newest".

What goes wrong:

Instead: deploy tags that never move (2.14.1, or the git commit id), and pin the digest (name@sha256:...) for anything critical - the digest is the hash of the image's manifest, so it can only ever mean those exact bytes. docker images --digests or docker inspect -f '{{index .RepoDigests 0}}' show it. Pin base images by digest too, and let a bot like Renovate propose updates.

Also asked: What is the difference between a tag and a digest? · What are the parts of an image reference like registry.lab/team/orders:2.14.1? · Does the CREATED column of docker images show when you pulled the image?

Practise this lesson in the terminal Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.