DEV Community

Cover image for Popular projects SHA-pin GitHub Actions 67.6% of the time — but Docker base images only 7.6%
hamelin123
hamelin123

Posted on Originally published at zennoxa.com

Popular projects SHA-pin GitHub Actions 67.6% of the time — but Docker base images only 7.6%

Pinning a dependency to something that can't change underneath you is the same idea whether it's a CI action or a container base image. We already measured how often popular projects pin their GitHub Actions to a commit SHA. So we asked the identical question one layer down: how often do they pin their Docker base images to an immutable @sha256 digest?

TL;DR

  • Across 25 popular infra/app projects (250 external base-image references), only 7.6% are pinned to an immutable @sha256 digest (19 refs).
  • 81.6% use a mutable version tag like node:20.1.2 (204 refs); 10.8% are :latest/untagged (27 refs).
  • Only 5 of 22 projects that ship Dockerfiles use @sha256 pinning for any base image.
  • Same supply-chain principle, measured before: GitHub Actions pinned to a commit SHA = 67.6%. That's roughly 9× more adoption for Actions than for container digests.
  • Point-in-time (2026-07-21), reproducible across all 25 repos with public tooling.

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%

The 5 projects that digest-pin any base image: posthog, Ghost, elasticsearch, moby, n8n.

Side by side with the Actions study:

Immutability practice Adoption across popular repos
GitHub Actions pinned to a commit SHA 67.6%
Docker base images pinned to a digest 7.6%

Why it matters

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:…. It's the exact same risk that made SHA-pinning a CI checklist item after the 2025 GitHub Actions compromises (tj-actions, reviewdog) — just one layer down, and adopted ~9× less often.

Our read on the gap: the Actions ecosystem had a forcing function (those compromises) plus a one-click habit (Dependabot). 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. Some pinned/unpinned images are also 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
Enter fullscreen mode Exit fullscreen mode

Then let Dependabot (package-ecosystem: docker) bump the digest so you stay patched without tracking a moving tag.

How we measured it

  • 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, --platform flags, build-args, and scratch), and classify as digest, version tag, or :latest/untagged.
  • Cross-check: Shield's CONTAINER-002 (:latest) rule against an independent FROM parser — the :latest counts 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.

Reproduce it yourself:

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:'
Enter fullscreen mode Exit fullscreen mode

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, --platform flags and build-args are excluded; one residual cross-file alias (~0.4%) may remain and doesn't move the headline.

Full per-repo dataset, method, and the cross-study comparison are on the canonical page: Do popular projects pin their base images? We checked 25. Zennoxa Research publishes reproducible security data on public projects with public tooling — no third-party scanner involved.

Top comments (0)