EduFlow

Section 2 · Production-Grade Images

Tags, Digests and Registries

18 min4 min readPut in order

Lesson at a glance

Name, version and publish notes-api images so every deploy is traceable, pin images by digest, and build for both amd64 and arm64 so the cluster can run what your laptop built.

  1. 1Anatomy of an image reference
  2. 2A tagging scheme that holds up
  3. 3Publish to a registry
  4. 4Multi-platform images
  5. 5Pin by digest

No video here — just the real thing: a written lesson, runnable examples, a quiz and practice.

At 11 p.m. on a release night, two Pods of the same service gave different answers to the same request. Both were running api:latest. Someone had re-pushed latest in the afternoon, and one node had pulled the new image while the other was still using its cached copy. We spent an hour proving the code was fine before anyone looked at the image. Names are part of your deployment, so make them precise.

Anatomy of an image reference

Text
ghcr.io/skylane/notes-api:1.4.0@sha256:3f1c9e…
└──┬──┘ └──┬──┘ └───┬───┘ └─┬─┘ └─────┬─────┘
registry  namespace  repo    tag      digest
  • The registry is the server that stores images: Docker Hub (the default when you leave it out), GitHub Container Registry (ghcr.io), Amazon ECR, Google Artifact Registry and so on.
  • The tag is a human-friendly label. It’s a pointer, and pointers can move.
  • The digest is a SHA-256 hash of the image manifest (or, for a multi-platform image, of its image index), which in turn lists the hash of every layer. Change one byte anywhere and the digest changes. It can’t be moved.

notes-api:latest isn’t special. latest is just the tag Docker uses when you don’t specify one. It doesn’t mean “newest”, and nothing updates it unless someone pushes it.

A tagging scheme that holds up

For each build that might be deployed, push two tags for the same image:

  • A version people can talk about: 1.4.0.
  • The commit it came from: sha-9f2c1ab.

When someone reports a bug in “1.4.0”, the SHA tag takes you straight to the code. And the rule that prevents the release-night incident: never push different content under a tag that’s already deployed. If you fix something, that’s 1.4.1. Many registries can enforce this; Amazon ECR, for example, has a tag immutability setting that rejects a push to an existing tag.

Build once, promote many times. The image that passes staging should be the exact image that reaches production, identified by the same digest. Don’t rebuild from the same commit for production “to be safe”: a rebuild can pull a newer base image or a different transitive dependency, and then production runs something no one tested. Promotion is a change to which digest each environment’s manifest points at, not a new build.

Keep the registry tidy too. Retention rules that delete untagged images and old SHA tags after a few months keep storage costs down, but exclude anything a manifest still references, or your next rollback will fail on a missing image.

Publish to a registry

Terminal
echo "$GHCR_TOKEN" | docker login ghcr.io -u amara-skylane --password-stdin

GIT_SHA=$(git rev-parse --short HEAD)
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t ghcr.io/skylane/notes-api:1.4.0 \
  -t ghcr.io/skylane/notes-api:sha-$GIT_SHA \
  --push .

docker buildx imagetools inspect ghcr.io/skylane/notes-api:1.4.0

--password-stdin keeps the token out of your shell history. In CI, use the platform’s short-lived token rather than a personal one. The last command prints the image’s digest and the platforms it contains.

Multi-platform images

Your laptop may be arm64 (Apple Silicon) while the cluster’s nodes are amd64, or the reverse. An image built for one CPU architecture fails on the other with the terse exec format error. A multi-platform build fixes that: --platform linux/amd64,linux/arm64 builds both variants and pushes an image index, a small list that maps each platform to its own manifest. When a node pulls notes-api:1.4.0, the runtime picks the manifest for its own architecture.

The tag 1.4.0 points to an image index digest. The index lists an amd64 manifest and an arm64 manifest, each listing its own layers. tag 1.4.0 image index sha256:3f1c… linux/amd64 manifest layers for x86 nodes linux/arm64 manifest layers for ARM nodes movable fixed
One tag points to an index; each node pulls the manifest for its CPU

Building for a foreign architecture runs under emulation, which can be slow for heavy npm ci steps. CI runners with native arm64 and amd64 machines are faster when builds grow. To keep multi-platform images locally, Docker needs the containerd image store, which is the default on new Docker Desktop installs; check that it’s turned on in older setups.

Pin by digest

A digest guarantees every node runs identical bytes, whatever happens to the tag later. You can deploy with both, and the digest wins:

YAML
image: ghcr.io/skylane/notes-api:1.4.0@sha256:3f1c9e…

The tag stays in the reference for humans; the runtime pulls by digest. Do the same for base images in your Dockerfile, so a rebuild next month doesn’t silently pick up a different node:24-slim:

Dockerfile
FROM node:24-slim@sha256:<digest-from-imagetools-inspect> AS build

Pinning trades automatic patches for predictability, so pair it with a bot such as Dependabot or Renovate that opens a pull request when a new base digest is published. You get updates, reviewed and tested, instead of surprises.

Your image is now named, versioned and published. Before anyone deploys it, you should know what’s inside it, which is the next lesson.

Key takeaways

  • An image reference is registry/namespace/repository:tag@digest; the tag is a movable label, the digest is the content’s fingerprint.
  • latest is only the default tag name, not the newest build; deploy explicit versions such as 1.4.0 or a Git SHA.
  • Never push different content under a tag that’s already deployed; enable tag immutability where your registry supports it.
  • Pinning by digest (@sha256:…) guarantees every node runs the same bytes, for your app and for base images.
  • Build multi-platform images with docker buildx build --platform linux/amd64,linux/arm64 when developers and servers use different CPUs.

Further reading

Check your understanding

4 questions

Question 1 of 4

Two Pods of the same Deployment, both using notes-api:latest, behave differently. What’s the most likely explanation?

Question 2 of 4

What does a digest such as sha256:3f1c… identify?

Question 3 of 4

A developer builds notes-api on an Apple Silicon laptop and pushes it. On the amd64 cluster the Pod logs exec format error. What’s the fix?

Question 4 of 4

Which tagging scheme gives the best traceability for notes-api releases?

Practice

Put in order

Ship release 1.4.0

Put the release steps for notes-api 1.4.0 in order, from source code to a verified deployment that every node runs byte-for-byte.

Use the arrow buttons to reorder.

  1. 1kubectl rollout status deployment/notes-api
  2. 2docker buildx imagetools inspect ghcr.io/skylane/notes-api:1.4.0 to read the digest
  3. 3Push both tags to ghcr.io
  4. 4docker buildx build --platform linux/amd64,linux/arm64 -t ghcr.io/skylane/notes-api:1.4.0 -t ghcr.io/skylane/notes-api:sha-9f2c1ab .
  5. 5Set image: ghcr.io/skylane/notes-api:1.4.0@sha256:… in the Deployment manifest
  6. 6kubectl apply -f k8s/
  7. 7Run the test suite and a scan against the built image