Pin a Docker image to a digest in a Dockerfile
A tag like node:22 is a label the publisher can move to a different image tomorrow. A digest names exact content. This guide shows how to find the right digest, what to write in the FROM line, the multi-architecture trap, and how to keep the pin up to date.
What to write
Keep the tag and add the digest: FROM node:22-alpine@sha256:0a7108bf6c7bf5de370ffb1a3ed6be93d405b43ff159f681a8d18c0e2bc2e402. Docker uses the digest and ignores the tag, but the tag stays readable for you and for tools such as Renovate and Dependabot that read it to propose updates (Docker's build best practices recommend pinning base images to digests for reproducible builds). Writing only the digest works too, but nobody reading the file can tell which release it is.
How to find the digest
With Docker installed, ask the registry without pulling the image:
docker buildx imagetools inspect node:22-alpine
The line labelled Digest: is the one to use. It is the digest of the image index (the list of per-platform images), and that is the right one to pin: it lets Docker choose the correct platform on an Intel laptop, an Apple-silicon Mac and an ARM server. Do not copy the digest of a single platform entry from the Manifests: list below it unless you want to lock the build to that CPU architecture.
Without Docker, the registry's HTTP API gives the same value in the Docker-Content-Digest header of a HEAD request for the manifest, after fetching an anonymous pull token. That is what Pinfix does. On 2026-10-17 it returned the same digest as docker buildx imagetools inspect for all six images we compared: alpine:3.20, node:22, node:22-alpine, ghcr.io/astral-sh/uv:latest, mcr.microsoft.com/dotnet/runtime:8.0 and quay.io/prometheus/prometheus:latest.
Do it for a whole Dockerfile
This is real output from pinfix pin Dockerfile on a Dockerfile with three base images on three registries (Docker Hub, GitHub Container Registry and Microsoft's registry). Digests change when publishers push, so yours will differ if you run it later.
Dockerfile
line 1
- node:22-alpine
+ node:22-alpine@sha256:0a7108bf6c7bf5de370ffb1a3ed6be93d405b43ff159f681a8d18c0e2bc2e402
line 4
- ghcr.io/astral-sh/uv:latest
+ ghcr.io/astral-sh/uv:latest@sha256:4a1f3666816dafadd20ceb71eac9a7d56d7ef30279f7d64048519ab901ada336
line 5
- mcr.microsoft.com/dotnet/runtime:8.0
+ mcr.microsoft.com/dotnet/runtime:8.0@sha256:60d11c2be381e7f90b8bd984472dd2432933858aba55087d5936980cf2071d5a
3 references would change. Run again with --write to save.
Cases to leave alone
FROM scratchhas no image to pin.FROM build AS testrefers to an earlier stage of the same Dockerfile, not an image.FROM ${BASE}gets its value at build time, so pin the value where it is defined.
Keeping a pin current
A pin freezes the image, including its unpatched vulnerabilities. Something has to move it forward: Renovate or Dependabot opening update pull requests, or a regular manual refresh. Pinfix Pro includes pinfix verify (reports pins whose tag now points at a newer image) and pinfix update (refreshes the digest of the same tag). Pinning is for repeatable builds and for being sure what you run; it is not a substitute for updating.
Try it
The Pinfix command line tool and VS Code extension are free for finding and pinning one file at a time: npx @plainworkdigital/pinfix check lists unpinned images and actions with no network access. The free web checker covers GitHub Actions lines.