Do popular projects pin their base images? We checked 25
Popular projects SHA-pin their GitHub Actions two-thirds of the time — but pin their Docker base images to an immutable digest only 7.6% of the time. Same supply-chain idea, a fraction of the adoption. Reproducible data across 25 repos.
TL;DR — Across 25 popular infrastructure and application projects (250 external
base-image references), only 7.6% are pinned to an immutable @sha256 digest.
92.4% use a mutable reference. Compare that to our
GitHub Actions pinning study, where 67.6% of
action references were SHA-pinned: it's the exact same supply-chain idea — pin to
something that can't change underneath you — adopted ~9× less often for
containers than for CI actions.
The same problem, one layer down
We already looked at GitHub Actions pinning: a
uses: action@v4 runs whatever that mutable tag points at today, so the hardened
practice is to pin to an immutable commit SHA. A Docker base image is the identical
risk one layer down. FROM node:20 (or worse, FROM node:latest) pulls whatever
image that tag resolves to at build time. Tags are mutable — the same node:20
can be a different image tomorrow — so a reproducible, tamper-evident build pins to
the content digest: FROM node@sha256:….
So we asked the same question of containers that we asked of Actions: how often do popular projects actually pin?
What we measured
- Corpus: 25 popular infra/app repos; 22 ship Dockerfiles with external base images.
- Per repo: sparse-checkout the Dockerfiles, parse every external base-image
FROM(excluding internal stage aliases,--platformflags, build-args, andscratch), and classify the reference as digest (@sha256:…, immutable), version tag (node:20.1.2, mutable), or:latest/untagged (most mutable). - Tool: Shield's
CONTAINER-002(:latest) cross-checked against an independentFROMparser; the:latestcounts agreed (16 vs 15, one build-stage edge case). Digest classification is unambiguous — a reference either contains@sha256:or it doesn't — and we hand-verified samples at both ends (posthog'spython@sha256:355b…vs grafana'sgolang:latest).
Full per-repo dataset: data.json.
The numbers
| Pin type | Refs | Share |
|---|---|---|
Digest (@sha256, immutable) |
19 | 7.6% |
Version tag (node:20.1.2) |
204 | 81.6% |
:latest or untagged |
27 | 10.8% |
| Total external base images | 250 | 100% |
- 7.6% digest-pinned. Only 5 of 22 projects use
@sha256pinning for any base image (posthog, Ghost, elasticsearch, moby, n8n). - 81.6% version tags. The bulk of the ecosystem sits here — reasonable and readable, but still mutable (a tag can be repushed).
- 10.8%
:latest/untagged — the genuinely non-reproducible end (FROM golang:latest,FROM busybox).
The cross-study point
Put the two studies side by side:
| Immutability practice | Adoption across popular repos |
|---|---|
| GitHub Actions pinned to a commit SHA | 67.6% |
| Docker base images pinned to a digest | 7.6% |
Same principle, ~9× the adoption for Actions. Our read: the GitHub Actions compromises of 2025 (tj-actions, reviewdog) put SHA-pinning on every CI checklist, and Dependabot made it a one-click habit. The base-image equivalent hasn't had its forcing function yet — and digest strings are ugly to write by hand, so most teams stop at a version tag.
A fair caveat
Version tags are not "wrong." node:20.1.2 is readable, gets patch updates, and
is fine for most teams. Only a @sha256 digest is truly immutable, so we report it
as the strict gold standard for reproducible/tamper-evident builds — not as a claim
that everyone on a version tag is insecure. The clearly-risky category is
:latest/untagged, which makes builds non-reproducible and lets the base change
silently. Note too that some pinned/unpinned images are a project's own first-party
images, where the trust model differs.
How to pin
# mutable — resolves at build time, can change underneath you
FROM python:3.13-slim
# immutable — the exact image content, human-readable tag kept in a comment
FROM python:3.13.13-slim-bookworm@sha256:355bfa66770995d7e9a0da4b3473b44d0cb451f6b56f5615ad9c39e3c4eca03f
Then let Dependabot (package-ecosystem: docker) or a tool like pin-github-action's
container equivalent bump the digest so you stay patched without tracking a moving tag.
Shield flags :latest base images (CONTAINER-002) today.
Limitations
- 25 popular repos, point-in-time (2026-07-21) — not a random sample; large projects skew more disciplined, so the ecosystem-wide digest rate is plausibly lower.
- We classify references, not unique images; a reused base weights a repo's count.
- Stage aliases,
--platformflags and build-args are excluded; one residual cross-file alias (~0.4%) may remain and doesn't move the headline.
Reproduce it
git clone --depth 1 --filter=blob:none --sparse https://github.com/grafana/grafana g
cd g && git sparse-checkout init --no-cone && git sparse-checkout set '**/Dockerfile' && git checkout
# a base image is immutable only if the ref contains @sha256:
grep -rhE '^\s*FROM\s' --include='*Dockerfile*' . | grep -v '@sha256:'
Per-repo counts and method in data.json.
Zennoxa Research publishes reproducible security data on public projects with public tooling. No third-party scanner is involved.