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.
- 1Anatomy of an image reference
- 2A tagging scheme that holds up
- 3Publish to a registry
- 4Multi-platform images
- 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
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
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.
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:
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:
FROM node:24-slim@sha256:<digest-from-imagetools-inspect> AS buildPinning 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. latestis only the default tag name, not the newest build; deploy explicit versions such as1.4.0or 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/arm64when developers and servers use different CPUs.