open atlas
↑ Back to track
Docker, containers as a system DOCK · 05 · 04

Reproducible builds: SOURCE_DATE_EPOCH, digest-pinned bases, and why a churning layer digest breaks signing

A reproducible build yields byte-identical layer digests from the same source on any machine. Embedded timestamps, unpinned base tags, and checkout mtimes break it — churning digests and undermining signing. SOURCE_DATE_EPOCH and digest-pinned bases are the levers.

DOCK Senior ◷ 16 min
Level
FoundationsJuniorMiddleSenior

The supply-chain audit asked a simple question and the team could not answer it: “Rebuild release v2.4.1 from the tagged commit and show us the same image digest you signed.” They checked out the exact tag, ran the exact build command — and got a completely different image digest. Every layer hash was different. The signed image in the registry and the rebuilt one shared not a single layer, so there was no way to prove the deployed artifact corresponded to the audited source. The cause was mundane: the Dockerfile used FROM node:22 (an unpinned tag that had moved since the release), COPY . . recorded the checkout’s file mtimes into the layer tarball, and the build stamped BUILD_TIME=$(date) into an env var. Three sources of non-determinism, each enough to change a digest. The image was perfectly functional both times — same code, same behavior — but not reproducible, and reproducibility is the property the signature was supposed to guarantee. A signature over a digest you cannot reproduce proves nothing about the source.

What “reproducible” means and why digests churn

If your team signs release images, ask: can you rebuild the exact same digest from the tagged commit today? If you cannot answer yes, the signature you are shipping proves less than you think. Reproducible means: the same source, built anywhere, any day, produces an image with the same layer digests down to the byte. Because everything in OCI is content-addressed, a single differing byte anywhere in a layer tarball changes that layer’s sha256 and every digest above it. Three usual suspects inject those differing bytes:

  • Embedded timestamps. BUILD_TIME=$(date), a build-info file with the current time, or a compiler that stamps the build date — any of these writes a value that changes every second, so the layer never reproduces.
  • File mtimes in the archive. When COPY . . writes files into a layer, the layer tarball records each file’s modification time. A fresh git checkout sets mtimes to now, so the same source copied on two days produces two different tarballs with identical contents but different timestamps — different digest.
  • Unpinned base images. FROM node:22 resolves to whatever digest the tag points at today; the tag moves on every minor patch, so a rebuild months later pulls a different base and inherits its different layers.

The fix for each is direct. Pin the base by digest: FROM node:22@sha256:abc... so the base never moves. Drive timestamps from SOURCE_DATE_EPOCH — a standardized environment variable holding a fixed Unix time (typically the commit’s author date) that build tools honor instead of the wall clock. And have BuildKit normalize the mtimes it writes.

# syntax=docker/dockerfile:1
FROM node:22@sha256:1a2b3c...      # pinned: base never silently moves
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Feed a fixed time and ask BuildKit to rewrite layer timestamps to it:
SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) \
  docker buildx build \
    --output type=image,name=app:v2.4.1,rewrite-timestamp=true .

rewrite-timestamp=true tells BuildKit to set every file’s mtime in the produced layers to SOURCE_DATE_EPOCH, erasing the checkout-time variance. With the base pinned, the clock pinned, and mtimes normalized, two builds of the same commit on two machines produce the same digests.

Quiz

Two builds of the exact same commit, on two machines, produce images with different layer digests. The code and behavior are identical. Which of these is NOT a likely cause?

Why this works

Why does signing care about reproducibility specifically? A signature (cosign, Notary) is computed over the image digest. The promise a consumer wants is: “this running image corresponds to that audited source.” If you cannot rebuild the audited source into the same digest, the signature degrades to “someone with the key signed some bytes” — it no longer ties the artifact to a source you can inspect. Reproducibility is what makes the signature verifiable: an auditor (or an SLSA provenance check) rebuilds from source, gets the identical digest, and confirms the signed artifact is exactly that source with nothing injected. Non-reproducible builds also quietly defeat the cross-stage and registry cache — a churning digest means COPY —from and pushed layers miss every time — so reproducibility pays in both security and build speed.

Determinism beyond the three big levers

Pinning the base, the clock, and mtimes handles most cases, but a few subtler non-determinisms remain worth knowing. Package installs that do not pin versions (apt-get install curl without =version, pip install without a lockfile) pull whatever is current, so freeze versions or use a lockfile. Build steps that emit output in filesystem-iteration order can vary; tools increasingly sort deterministically, but a custom script may need an explicit sort. Embedded build paths (a compiler recording /home/runner/...) differ across machines; many toolchains accept a path-prefix remap. And network access during build is inherently non-reproducible — the same curl https://... can return different bytes tomorrow — which is why hermetic builds fetch only pinned, checksummed inputs. The senior posture: treat every input to the build as something that must be pinned or normalized, and assume any unpinned input will eventually move and break your digest when you least want it to.

Quiz

Your security team signs release images with cosign. Why does a non-reproducible build undermine the value of that signature, even though the signing key is secure?

Recall before you leave
  1. 01
    Name the three classic sources of build non-determinism and the lever that fixes each.
  2. 02
    Explain why reproducibility is what makes an image signature meaningful, and what else a churning digest breaks.
Recap

Reproducible means the same source, built on any machine on any day, yields an image with byte-identical layer digests. OCI is content-addressed, so one differing byte in a layer tarball changes that layer’s sha256 and every digest stacked above it, and three usual suspects inject that byte. Embedded timestamps — BUILD_TIME=$(date), a build-info file, a compiler stamping the date — change every second. File mtimes recorded into the layer by COPY . . differ because a fresh git checkout sets them to now, so identical contents reproduce as different tarballs. And unpinned base tags like FROM node:22 resolve to whatever the tag points at today and move under you between builds. The levers are direct: pin the base by digest (FROM node:22@sha256:…), drive every build timestamp from SOURCE_DATE_EPOCH set to the commit’s author date, and run BuildKit with rewrite-timestamp=true so it normalizes every file mtime in the produced layers to that epoch; beyond the big three, pin package versions or use a lockfile, sort any iteration-ordered output, remap embedded build paths, and avoid networked build steps that fetch unpinned bytes. This matters most for the supply chain: a signature (cosign) is computed over the image digest, so if you cannot rebuild the audited commit into the same digest, the signature no longer proves the deployed artifact corresponds to inspectable source — exactly the audit the team failed when v2.4.1 rebuilt to a different hash. Reproducibility is what makes the signature verifiable, lets SLSA provenance checks confirm the artifact by rebuilding, and as a bonus keeps cross-stage COPY —from and registry layer caches hitting instead of churning a fresh layer for every functionally-identical build. Now when you set up a release pipeline, make “can we reproduce this digest?” a required gate — pin the base, feed SOURCE_DATE_EPOCH, add rewrite-timestamp=true, and verify by rebuilding the tagged commit before trusting the signature.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 6 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.