Skip to Content
Docker Advanced: Builds and Containers That Hold Up in Production
Advanced
0 likes
Courses
Docker Advanced: Builds and Containers That Hold Up in Production

Pin Digests, Base Image Updates and CVEs

Look up an image digest, pin a base image with it, and scan an image without a Docker account. After this, you can get repeatable builds and still take security updates on purpose.

Course content

Sign in to save your progress.

Course Home

Skip ahead: If your Dockerfiles already pin base images by digest and something updates those digests on a schedule, skip to "Scanning: what it can and cannot tell you".

FROM alpine:3.24 looks pinned. It is not. A tag is a name that the publisher can move, and official images move their tags on purpose, every time they rebuild for security fixes. That is good for security and bad for repeatability: the same Dockerfile can build a different image next week. A digest fixes the exact image. The real question is not "tag or digest" but how you get both repeatable builds and security updates.

I ran everything here on Docker Desktop 4.85.0 with Engine 29.6.2 on 21 October 2026. The digests you see will be different on your machine if the publisher has rebuilt the image since then. That is the point of this lesson.

What a digest is

A digest is the SHA-256 hash of the image's manifest. Change one byte in any layer and the digest changes. Ask the registry what alpine:3.24 points at right now:

docker buildx imagetools inspect alpine:3.24 | Select-Object -First 3
Name:      docker.io/library/alpine:3.24
MediaType: application/vnd.oci.image.index.v1+json
Digest:    sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6

That Digest is for the index, the list that points at one image per platform (amd64, arm64 and so on). It is the one to pin, because it works on every platform the image supports. Your local copy records the same value:

docker image inspect alpine:3.24 --format "{{index .RepoDigests 0}}"
alpine@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6

Pin it

Put the digest after the tag. Docker uses the digest and treats the tag as a comment for humans, which is why you keep both.

FROM alpine:3.24@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6
RUN cat /etc/alpine-release
Set-Location pin-demo
docker build -q -t pinned-app . | Out-Null
docker run --rm pinned-app cat /etc/alpine-release
3.24.2

This Dockerfile will build from exactly the same base in a year, as long as the registry still has that image. Official images keep old digests available for a long time, but there is no promise of forever.

What happens if the digest is wrong, for example after a bad copy and paste?

FROM alpine:3.24@sha256:0000000000000000000000000000000000000000000000000000000000000000
docker build -q -f Dockerfile.bad -t pinned-bad . 2>&1 | ForEach-Object { "$_" } | Select-String "not found" | Select-Object -First 1 | ForEach-Object { $_.Line.Trim() }
not found

The build stops. That is the second benefit of a digest: nobody can quietly swap the image behind it. If the registry returned a different image for that digest, the hash would not match and Docker would refuse it.

The cost: you stop getting fixes

Here is the part people forget. Once you pin, you no longer get the publisher's security rebuilds. alpine:3.24 may get a fixed OpenSSL next week, and your build will keep using the old one, because that is exactly what you asked for.

So pinning is only half of the job. The other half is updating the pin on purpose:

  1. Something checks for a new digest on a schedule. Dependabot and Renovate both understand FROM image:tag@sha256:... lines and open a pull request when the tag moves. I did not set either up for this lesson.
  2. The pull request runs your normal build and tests.
  3. A person merges it, and the change is in your Git history with a date.

Without step 1, a pinned base image just gets older and older. In my opinion that is worse than not pinning, because it looks safe.

⚠ Warning: Don't pin by digest and then never look at it again. A digest from last year is a known set of last year's vulnerabilities.

Which tag to pin

Pin a tag that matches how often you want changes. alpine:3.24 gets patch rebuilds. alpine:3 would jump to 3.25 one day. alpine:latest can be anything. For base images, pin the minor version (3.24, 3.14-alpine, 1.30-alpine) plus the digest, and move to the next minor as a planned change.

Scanning: what it can and cannot tell you

A vulnerability scanner reads the packages in an image and compares them with public CVE lists. Docker Desktop includes Docker Scout, but its CVE commands ask you to sign in with a Docker account. To keep this lesson account-free I used Grype, an open-source scanner, in a container. It pulls the image from the registry itself, so it needs no access to your Docker engine:

docker run --rm anchore/grype:v0.120.1 alpine:3.24 --only-fixed -q -o table 2>$null
"exit code $LASTEXITCODE"
NAME  INSTALLED  FIXED IN  TYPE  VULNERABILITY   SEVERITY  EPSS         RISK
zlib  1.3.2-r0   1.3.2-r1  apk   CVE-2026-85091  High      0.4% (27th)  0.3
exit code 0

On the day I ran it, Grype listed one finding with a fix: zlib in Alpine's own packages, fixed in a newer package release. Your result will differ, and it will differ next week on the same image. The image does not change. The CVE database does. That is why a scan result is a snapshot with a date, never a property of the image. I showed only fixed vulnerabilities (--only-fixed), because those are the ones you can act on by updating.

How to read a scan without panicking:

  • Look at fixed vulnerabilities in packages your program actually uses first. A CVE in a tool your app never runs is real but less urgent.
  • A small base image has fewer packages and so fewer findings. The multi-stage lesson is also a security lesson.
  • "No vulnerabilities found" means none known today, in packages the scanner can see. A binary you copied in by hand may be invisible to it.

💡 Tip: Scan in CI after the build, and fail the build only on severities you are ready to fix, for example "critical with a fix available". A gate that fails on everything gets switched off within a month.

Common mistakes

  • Pinning the digest of one platform (from docker image inspect on an older engine) instead of the index. ARM machines then fail to build.
  • Removing the tag and keeping only the digest. Nobody can tell what sha256:294b... is supposed to be.
  • Treating a clean scan as a security review. It is one input.

If a new OpenSSL fix came out tomorrow, how many days would it take before the images you run in production actually had it, and who would notice?

About

Look up an image digest, pin a base image with it, and scan an image without a Docker account. After this, you can get repeatable builds and still take security updates on purpose.

Duration
10 min
Last update
08/01/2026
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.