the same source code always produces the same digest
Reproducible Docker builds produce the same image digest from identical source code and identical configuration on every build, no matter when or on which machine the build ran. SOURCE_DATE_EPOCH eliminates the biggest source of noise behind non-determinism, the current timestamp embedded into every layer.
Table of Contents
- 1. Why the same build produces a different result twice
- 2. SOURCE_DATE_EPOCH: the key variable against timestamp noise
- 3. BuildKit support for deterministic layers
- 4. Pinning base images by digest instead of by tag
- 5. Deterministic dependencies: lockfiles and fixed registries
- 6. Filesystem ordering and metadata consistency
- 7. Verifying reproducibility: rebuild and digest comparison
- 8. Enforcing reproducible builds in the CI pipeline
- 9. Deterministic vs. non-deterministic build elements
- 10. Summary
- 11. FAQ
1. Why the same build produces a different result twice
A reproducible Docker build produces exactly the same image digest from the same source code, the same Dockerfile and the same dependency versions on every run, whether the build happens today or in a year, on a laptop or on a CI runner. In practice this is surprisingly rare: a second build of the same commit almost always produces a different digest, because timestamps, file orderings and environment variables flow into every layer.
This lack of reproducibility becomes a problem at the latest when a security incident has to be investigated and a team wants to prove that a production image actually originates from a specific, reviewed commit. Without reproducible Docker builds, this chain can only be established through trust in the build infrastructure, not through independent verification. The Reproducible Builds project, originally coming from the Debian and Linux kernel world, created standards for exactly this problem that are now also relevant for container images.
The single most important lever for reproducible Docker builds is the environment variable SOURCE_DATE_EPOCH, a Unix timestamp standardized by the Reproducible Builds initiative that build tools use instead of the actual system time for all time information embedded in artifacts. Without this variable, virtually every built artifact carries the exact second of its creation inside itself, which alone causes almost every second build to produce a different digest.
2. SOURCE_DATE_EPOCH: the key variable against timestamp noise
SOURCE_DATE_EPOCH contains a Unix timestamp in seconds, typically the commit date of the source code revision being built. Instead of calling date +%s at build time, compatible tools read this variable and consistently use its value for file timestamps in archives, for build times embedded in binaries, and for the image creation time stored in the OCI manifest.
For reproducible Docker builds, SOURCE_DATE_EPOCH is set deterministically from the git commit timestamp, not from the current system time, so that the same commit always produces the same value on rebuild. BuildKit itself has explicitly supported a SOURCE_DATE_EPOCH build argument since version 0.11, which fixes the timestamps of all produced layers and the final manifest to that value instead of using the actual build time.
# Derive SOURCE_DATE_EPOCH deterministically from the git commit timestamp
export SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)
echo "Building with fixed epoch: $SOURCE_DATE_EPOCH"
# Pass it into buildx so BuildKit uses it for all layer timestamps
docker buildx build \
--build-arg SOURCE_DATE_EPOCH="$SOURCE_DATE_EPOCH" \
--output type=image,name=registry.mironsoft.de/shop-app:1.4.0,rewrite-timestamp=true \
.
3. BuildKit support for deterministic layers
Modern versions of BuildKit support the rewrite-timestamp feature in the output target, which retroactively resets all file timestamps inside a layer to the value of SOURCE_DATE_EPOCH. Without this feature, the actual creation time of every file in the layer is preserved, even if the manifest metadata itself is already deterministic, which leads to different layer digests despite identical file content.
A second important BuildKit mechanism for reproducible Docker builds is deterministic layer squashing through the frontend mode dockerfile.v1 with explicit ordering control. Parallel RUN instructions, which BuildKit executes concurrently by default, can finish in non-deterministic order, which can lead to different results for filesystem operations sharing target directories. For maximum reproducibility it is worth explicitly serializing critical steps.
# syntax=docker/dockerfile:1.7
FROM php:8.4-fpm AS base
ARG SOURCE_DATE_EPOCH
# Explicit, single RUN instruction avoids non-deterministic
# ordering effects from parallel BuildKit execution
RUN set -eu; \
apt-get update; \
apt-get install -y --no-install-recommends libzip-dev; \
rm -rf /var/lib/apt/lists/*
COPY --link composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist
4. Pinning base images by digest instead of by tag
A tag such as php:8.4-fpm points to different, changing digests over time, because maintainers push security updates back into the same tag. For reproducible Docker builds this is a direct contradiction: the same Dockerfile content can pull two entirely different base images on two different days. The solution is to reference base images by digest as a rule, not by tag.
The command docker pull --digest, or the direct notation FROM php:8.4-fpm@sha256:abc123..., fixes the base image to a specific, immutable content. Dependabot and Renovate support automated digest updates through pull requests, so security updates still flow in, but every single build point in time traceably references an exact, documented base image.
# WRONG for reproducibility: tag can silently point to a new digest
FROM php:8.4-fpm
# RIGHT: pinned by digest, always resolves to the exact same base layer
FROM php:8.4-fpm@sha256:9d3f1c8e2a4b7f6d0c9e8a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d
5. Deterministic dependencies: lockfiles and fixed registries
Even with a pinned base image and SOURCE_DATE_EPOCH, a build remains non-reproducible if composer install or npm install run without strict lockfiles and pull the newest compatible package version from the registry every time. composer.lock and package-lock.json fix exact versions including transitive dependencies and are a basic prerequisite for reproducible Docker builds, independent of the timestamp topic.
Composer itself also embeds a timestamp in generated autoload files, which under composer install --no-scripts --optimize-autoloader should be derived deterministically from package contents rather than from the current time. Private package registries should also never overwrite artifacts, a package once published under a fixed version must never change content again, otherwise reproducibility breaks retroactively for every build already made.
6. Filesystem ordering and metadata consistency
Tar archives, which Docker layers ultimately consist of, are inherently order dependent: the same set of files can be written into an archive in a different order and produce different byte sequences despite identical content. BuildKit sorts files inside a layer by path name by default, which makes the order deterministic, as long as no external tools outside this control produce files.
File permissions, owner IDs and extended attributes such as xattrs become additionally critical, as they can be set differently depending on the host operating system and Docker version. For reproducible Docker builds it is recommended to consistently use COPY --chown and explicit chmod calls inside the Dockerfile, rather than relying on host defaults that can vary between a developer laptop and a CI runner.
7. Verifying reproducibility: rebuild and digest comparison
The only reliable method to actually confirm reproducible Docker builds is a repeated build on different infrastructure followed by a digest comparison. A build on the local development laptop and a second build of the same commit on an independent CI runner must produce the same manifest digest if the build is truly deterministic.
The rebuilderd project, originally developed for reproducible Linux distribution packages, can be adapted for continuous rebuild verification of container images as well: an independent rebuilder regularly rebuilds the same image from the same source code and reports any digest deviation as a potential determinism bug or as a sign of a compromised build pipeline.
# Build twice from the same commit and compare digests
docker buildx build --build-arg SOURCE_DATE_EPOCH="$(git log -1 --format=%ct)" \
--output type=oci,dest=build1.tar .
docker buildx build --build-arg SOURCE_DATE_EPOCH="$(git log -1 --format=%ct)" \
--output type=oci,dest=build2.tar .
# Compare the resulting manifest digests
sha256sum build1.tar build2.tar
# Identical hashes confirm a bit-for-bit reproducible build
8. Enforcing reproducible builds in the CI pipeline
In the CI pipeline it is worth having a dedicated verification job that triggers a second, independent rebuild after the regular build and compares digests before an image is marked as released. This extra step costs build time, but delivers solid proof that reproducible Docker builds actually work and are not just theoretically possible.
Combined with image signing from Cosign and an SBOM from Syft, a complete chain of evidence emerges: the digest is reproducibly derivable from the source code, the signature proves origin from your own pipeline, and the SBOM documents the exact content. For regulated industries this combination is increasingly an explicit requirement, no longer just a best practice.
# GitLab CI: verify build reproducibility with an independent rebuild
verify-reproducible:
stage: verify
script:
- export SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)
- docker buildx build --build-arg SOURCE_DATE_EPOCH="$SOURCE_DATE_EPOCH" \
--output type=oci,dest=rebuild.tar .
- EXPECTED_DIGEST=$(cat expected-digest.txt)
- ACTUAL_DIGEST=$(sha256sum rebuild.tar | awk '{print $1}')
- test "$EXPECTED_DIGEST" = "$ACTUAL_DIGEST" || \
(echo "Reproducibility check failed" && exit 1)
9. Deterministic vs. non-deterministic build elements
Not every part of a Docker build is equally prone to non-determinism. The following table classifies the most common causes of diverging digests by how easy they are to fix.
| Build Element | Non-deterministic when | Deterministic Fix |
|---|---|---|
| Layer timestamps | System time at build time | SOURCE_DATE_EPOCH with rewrite-timestamp |
| Base image | Referenced by tag | Referenced by digest |
| Dependency versions | Installed without a lockfile | Strictly use composer.lock / package-lock.json |
| File order in layer | External tools outside BuildKit control | BuildKit default sort by path name |
| Parallel RUN steps | Concurrent filesystem conflicts | Explicitly serialize critical steps |
Most of these causes can be fixed with manageable effort once identified. The biggest practical lever remains SOURCE_DATE_EPOCH combined with digest pinning of base images, because these two measures alone eliminate the vast majority of typical non-determinism in Docker builds.
Mironsoft
Deterministic build pipelines and supply chain evidence
Reproducible Docker builds for your pipeline?
We analyze your Dockerfiles for non-determinism, set up SOURCE_DATE_EPOCH and digest pinning, and add a verification job that provably confirms reproducible builds inside the CI pipeline.
Determinism audit
Reviewing Dockerfiles for timestamps, tag references and non-determinism
SOURCE_DATE_EPOCH setup
Setting up BuildKit configuration for deterministic layers and manifests
CI verification
Integrating automated rebuild-and-digest comparison into the pipeline
10. Summary
Reproducible Docker builds with SOURCE_DATE_EPOCH solve a fundamental trust problem: proving that a production image actually originates from a specific, reviewed source code state, without requiring blind trust in the build infrastructure. SOURCE_DATE_EPOCH eliminates the single biggest cause of diverging digests by replacing the current system time with a fixed timestamp derived from the commit.
Combined with digest based pinning of base images, strict lockfiles for dependencies and deterministic BuildKit configuration, reproducible Docker builds can be reliably achieved in practice. Verification through an independent rebuild with digest comparison turns the theoretical possibility into a solid, automatically checkable proof inside the CI pipeline.
Reproducible Docker Builds with SOURCE_DATE_EPOCH — Key Takeaways
SOURCE_DATE_EPOCH
A fixed timestamp derived from the git commit instead of the current system time, prevents timestamp noise in layers.
Digest pinning
Reference base images via @sha256: instead of by tag, prevents unnoticed base image changes.
Lockfiles
Strictly use composer.lock and package-lock.json, fixes exact dependency versions including transitive packages.
Verification
An independent rebuild with digest comparison as a CI job confirms actual reproducibility.