What does setting SOURCE_DATE_EPOCH change about a BuildKit container image build?
answer
- Take the clock out of the build
- A Unix timestamp, not a flag
- Set it from the commit timestamp
- Rewrites created and layer file mtimes
- Pins nothing; cached layers stay stale
basics
~10 sSOURCE_DATE_EPOCH is a Unix timestamp BuildKit uses instead of the wall clock: it writes that value into the image config's created and history fields and normalises file timestamps in the layers the build produces.
solid answer
~40 sIt removes the clock as a source of build-to-build variance. `SOURCE_DATE_EPOCH` is a cross-ecosystem convention - a Unix timestamp in seconds - that BuildKit honours: the image config's `created` field, the `history` entries and the mtimes of files written into new layers all take that value instead of the current time. Pipelines set it from the commit being built, typically `export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)`; `docker buildx build` reads it from the environment, and it can also be passed as a build argument. What it does not do matters just as much: it pins nothing, so a moving `FROM` tag or an `apt-get install` still changes layer content, and layers reused from the build cache can carry timestamps from an earlier epoch - so a reproducibility check should build with `--no-cache`.
code
bash · 3 linesexport SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)
docker buildx build --no-cache --load -t fraud-scoring:repro .
docker image inspect --format '{{.Created}}' fraud-scoring:reprogo deeper
Know that it is an environment variable holding a Unix timestamp, and that build tools use it in place of the current time so rebuilt artefacts stop differing by their timestamps alone.
Explain exactly which fields it touches - the config's created and history entries plus mtimes in newly built layers - and how a pipeline derives the value from the commit being built.
Demonstrate the limits: it fixes one variance family and leaves unpinned inputs untouched, and a warm build cache can hand you layers stamped under a different epoch, so verification means a cold-cache double build.
Decide whether normalised timestamps are worth mandating estate-wide, what the pipeline standard is for deriving the value, and how you avoid a false sense of reproducibility when the real inputs are still unpinned.
### The problem it solves Two of the reasons a rebuilt image gets a new digest are pure clock readings. The image config has a `created` field, each entry of its `history` array has one, and every file written into a layer carries an mtime in the layer's tar stream. Build the same source twice, five minutes apart, on a cold cache, and all of those move - even when not one byte of application content changed. `SOURCE_DATE_EPOCH` exists to take the clock out of the build. ### What it is `SOURCE_DATE_EPOCH` is a cross-ecosystem convention, not a Docker invention: a Unix timestamp, in seconds, that a build tool should use anywhere it would otherwise read the current time. BuildKit implements it. When it is set, the builder writes that value into the image config's `created` field and into the `history` entries, and it normalises the timestamps of files in the layers the build produces, instead of stamping them with the moment of the build. The usual value is the timestamp of the commit being built, which is stable for a given source revision and is trivially derived in the pipeline: ``` export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) ``` `docker buildx build` picks the variable up from the environment; it can also be passed explicitly as a build argument (`--build-arg SOURCE_DATE_EPOCH=...`), which is the form to use when the value must cross into a builder that does not share your shell environment. ### What it does not do This is the half candidates miss, and it is the half that matters in an interview. It does not pin any input. `FROM eclipse-temurin:21-jre-noble` still resolves a mutable tag; `apt-get install` still installs whatever the mirror serves today; a `curl` of a tarball still fetches today's file. Those change layer *content*, and no amount of timestamp normalisation makes a different jar into the same jar. `SOURCE_DATE_EPOCH` removes one family of variance and leaves the other three - unpinned inputs, injected build metadata, and genuinely non-deterministic build steps - untouched. It does not retroactively fix layers that come from somewhere else. Base image layers keep the timestamps their own publisher gave them; the value applies to what this build creates. And it interacts awkwardly with the build cache. A layer reused from cache was materialised earlier, under whatever epoch was in effect then, and a cached reuse can therefore carry timestamps that do not match the epoch you set for this run. The practical consequences are that a reproducibility check should build with `--no-cache` (or a cache-free builder) rather than trusting a warm cache, and that a mixed warm/cold pipeline can produce two "reproducible" builds that still disagree. Newer BuildKit releases add an exporter attribute to rewrite timestamps on cached layers as well, which narrows the gap; treat the availability of that as version-dependent and verify on the builder you actually run. ### Where it sits in the sequence Think of reproducibility as layers of an onion. `SOURCE_DATE_EPOCH` is the outermost and cheapest: one exported variable, no change to what the image contains, and it eliminates the noise that otherwise makes every rebuild differ. Underneath it sit the expensive properties - a digest-pinned base, locked dependency versions, checksum-verified downloads, a build that does not touch the network at all. Setting the epoch first is still worth doing, because until the clock is out of the way you cannot tell a real content change from ordinary timestamp churn: every rebuild differs, so no difference is informative. ### A worked check For a Spring Boot fraud-scoring service whose dependency layer is 2.3 GB, the useful experiment is: ``` export SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct) docker buildx build --no-cache --load -t fraud-scoring:r1 . docker buildx build --no-cache --load -t fraud-scoring:r2 . docker image inspect --format '{{.Id}}' fraud-scoring:r1 fraud-scoring:r2 ``` If the two IDs match, the build is deterministic on this machine at this moment - which is a much weaker statement than "reproducible", because both runs saw the same package mirror seconds apart. If they differ, the epoch has already done its job: whatever is left is real, and worth chasing. ### Interview framing The candidates who impress do three things: they define the variable rather than reciting it, they set it from the commit timestamp rather than from `date`, and they immediately name the limits - unpinned inputs and cached layers - instead of presenting it as the answer to reproducibility. The ones who do not usually assert that setting it makes builds reproducible, full stop.
- You set SOURCE_DATE_EPOCH and two rebuilds still produce different digests. What do you look at next?Content, not clocks. Compare the two images' layer lists and find the first that differs, then check whether that step fetched anything: a moving `FROM` tag, an OS package install, a dependency resolution without a lockfile, a downloaded artefact. Also check whether one of the builds reused cached layers - a cache hit can carry timestamps from an earlier epoch, so rerun both with `--no-cache` before concluding.
- Why set it from the commit timestamp rather than to a fixed constant like zero?The commit timestamp is stable for a given source revision, so every rebuild of that revision agrees while different revisions still get distinguishable dates. A hardcoded constant works for bit-identity too, but it makes every image claim the same creation date, which destroys the ordering signal people rely on when reading `docker history` or comparing two images during an incident. Epoch 0 also confuses tools that display 1970 dates.
- Does SOURCE_DATE_EPOCH change the timestamps inside the base image's layers?No. It governs the layers this build creates; layers inherited from the base image keep the timestamps their publisher wrote. That is fine for reproducibility as long as the base is pinned by digest, because those layer blobs are then byte-identical on every build by definition.
saying these in an interview costs you the question
- Claims setting SOURCE_DATE_EPOCH makes any build reproducible
- Thinks it is a docker build command-line flag only
- Sets it from date rather than the commit timestamp
- Believes it pins package or base image versions
- Ignores that cache-reused layers can keep old timestamps
- Assumes the legacy non-BuildKit builder honours it