derselbe Quellcode ergibt immer denselben Digest
Reproduzierbare Docker Builds erzeugen aus identischem Quellcode und identischer Konfiguration bei jedem Build denselben Image-Digest, unabhängig davon, wann oder auf welcher Maschine gebaut wurde. SOURCE_DATE_EPOCH eliminiert das größte Rauschquelle für Nicht-Determinismus, den eingebetteten aktuellen Zeitstempel in jedem Layer.
Inhaltsverzeichnis
- 1. Warum derselbe Build zweimal ein anderes Ergebnis liefert
- 2. SOURCE_DATE_EPOCH: die zentrale Variable gegen Zeitstempel-Rauschen
- 3. BuildKit-Unterstützung für deterministische Layer
- 4. Basis-Images per Digest statt per Tag pinnen
- 5. Deterministische Abhängigkeiten: Lockfiles und feste Registrierungen
- 6. Dateisystem-Reihenfolge und Metadaten-Konsistenz
- 7. Reproduzierbarkeit verifizieren: Rebuild und Digest-Vergleich
- 8. Reproduzierbare Builds in der CI-Pipeline erzwingen
- 9. Deterministische vs. nicht-deterministische Build-Elemente
- 10. Zusammenfassung
- 11. FAQ
1. Warum derselbe Build zweimal ein anderes Ergebnis liefert
Ein reproduzierbarer Docker Build erzeugt aus demselben Quellcode, demselben Dockerfile und derselben Abhängigkeitsversion bei jedem Durchlauf exakt denselben Image-Digest, egal ob der Build heute oder in einem Jahr, auf einem Laptop oder einem CI-Runner stattfindet. In der Praxis ist das überraschend selten der Fall: Ein zweiter Build desselben Commits liefert fast immer einen anderen Digest, weil Zeitstempel, Dateireihenfolgen und Umgebungsvariablen in jeden Layer einfließen.
Diese fehlende Reproduzierbarkeit wird spätestens dann zum Problem, wenn ein Sicherheitsvorfall untersucht werden muss und ein Team beweisen möchte, dass ein produktiv laufendes Image tatsächlich aus einem bestimmten, geprüften Commit stammt. Ohne reproduzierbare Docker Builds lässt sich diese Kette nur über Vertrauen in die Build-Infrastruktur herstellen, nicht durch unabhängige Nachprüfung. Das Reproducible-Builds-Projekt, ursprünglich aus dem Debian- und Linux-Kernel-Umfeld, hat für genau dieses Problem Standards geschaffen, die inzwischen auch für Container-Images relevant sind.
Der wichtigste einzelne Hebel für reproduzierbare Docker Builds ist die Umgebungsvariable SOURCE_DATE_EPOCH, ein von der Reproducible-Builds-Initiative standardisierter Unix-Zeitstempel, den Build-Werkzeuge anstelle des tatsächlichen Systemzeitpunkts für alle in Artefakten eingebetteten Zeitangaben verwenden. Ohne diese Variable trägt praktisch jedes gebaute Artefakt den exakten Sekundenzeitpunkt seiner Entstehung in sich, was allein schon jeden zweiten Build zu einem anderen Digest führt.
2. SOURCE_DATE_EPOCH: die zentrale Variable gegen Zeitstempel-Rauschen
SOURCE_DATE_EPOCH enthält einen Unix-Zeitstempel in Sekunden, typischerweise das Commit-Datum der Quellcode-Revision, aus der gebaut wird. Statt date +%s zur Build-Zeit aufzurufen, lesen kompatible Werkzeuge diese Variable und verwenden ihren Wert konsequent für Dateizeitstempel in Archiven, für in Binaries eingebettete Build-Zeitangaben und für die im OCI-Manifest gespeicherte Image-Erstellungszeit.
Für reproduzierbare Docker Builds setzt man SOURCE_DATE_EPOCH deterministisch aus dem Git-Commit-Zeitstempel, nicht aus der aktuellen Systemzeit, damit derselbe Commit bei jedem Rebuild denselben Wert liefert. BuildKit selbst unterstützt seit Version 0.11 explizit ein SOURCE_DATE_EPOCH-Build-Argument, das die Zeitstempel aller erzeugten Layer und des finalen Manifests darauf festlegt, statt die tatsächliche Build-Uhrzeit zu verwenden.
# 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-Unterstützung für deterministische Layer
Moderne Versionen von BuildKit unterstützen das Feature rewrite-timestamp im Output-Ziel, das alle Dateizeitstempel innerhalb eines Layers nachträglich auf den Wert von SOURCE_DATE_EPOCH zurücksetzt. Ohne dieses Feature bleibt der tatsächliche Erstellungszeitpunkt jeder Datei im Layer erhalten, selbst wenn die Manifest-Metadaten selbst schon deterministisch sind, was zu unterschiedlichen Layer-Digests trotz identischen Dateiinhalts führt.
Ein zweiter wichtiger BuildKit-Mechanismus für reproduzierbare Docker Builds ist deterministisches Layer-Squashing über den Frontend-Modus dockerfile.v1 mit expliziter Reihenfolge-Kontrolle. Parallele RUN-Anweisungen, die BuildKit standardmäßig nebenläufig ausführt, können in nicht-deterministischer Reihenfolge abschließen, was bei Dateisystem-Operationen mit gemeinsamen Zielverzeichnissen zu unterschiedlichen Ergebnissen führen kann. Für maximale Reproduzierbarkeit lohnt es sich, kritische Schritte explizit zu serialisieren.
# 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. Basis-Images per Digest statt per Tag pinnen
Ein Tag wie php:8.4-fpm zeigt über die Zeit auf unterschiedliche, sich ändernde Digests, weil Maintainer Sicherheitsupdates in dasselbe Tag nachziehen. Für reproduzierbare Docker Builds ist das ein direkter Widerspruch: Derselbe Dockerfile-Inhalt kann an zwei verschiedenen Tagen zwei völlig unterschiedliche Basis-Images ziehen. Die Lösung ist, Basis-Images grundsätzlich per Digest zu referenzieren, nicht per Tag.
Das Kommando docker pull --digest beziehungsweise die direkte Notation FROM php:8.4-fpm@sha256:abc123... fixiert das Basis-Image auf einen konkreten, unveränderlichen Inhalt. Dependabot und Renovate unterstützen automatisierte Digest-Updates über Pull Requests, sodass Sicherheitsupdates weiterhin einfließen, aber jeder einzelne Build-Zeitpunkt nachvollziehbar auf ein exaktes, dokumentiertes Basis-Image verweist.
# 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. Deterministische Abhängigkeiten: Lockfiles und feste Registrierungen
Selbst mit fixierten Basis-Images und SOURCE_DATE_EPOCH bleibt ein Build nicht reproduzierbar, wenn composer install oder npm install ohne strikte Lockfiles laufen und jedes Mal die aktuellste kompatible Paketversion aus der Registry ziehen. composer.lock und package-lock.json fixieren exakte Versionen inklusive transitiver Abhängigkeiten und sind eine Grundvoraussetzung für reproduzierbare Docker Builds, unabhängig vom Zeitstempel-Thema.
Composer selbst führt zusätzlich einen Zeitstempel in generierten Autoload-Dateien mit, der bei composer install --no-scripts --optimize-autoloader deterministisch aus den Paketinhalten statt aus der aktuellen Zeit abgeleitet werden sollte. Private Package-Registrierungen sollten zudem Artefakte niemals überschreiben, ein einmal veröffentlichtes Paket unter einer festen Version darf sich inhaltlich nie wieder ändern, sonst bricht die Reproduzierbarkeit rückwirkend für jeden bereits gebauten Build.
6. Dateisystem-Reihenfolge und Metadaten-Konsistenz
Tar-Archive, aus denen Docker-Layer letztlich bestehen, sind von Natur aus ordnungsabhängig: Dieselbe Dateimenge kann in unterschiedlicher Reihenfolge in ein Archiv geschrieben werden und erzeugt dabei unterschiedliche Byte-Sequenzen trotz identischen Inhalts. BuildKit sortiert Dateien innerhalb eines Layers standardmäßig nach Pfadname, was die Reihenfolge deterministisch macht, sofern keine externen Werkzeuge außerhalb dieser Kontrolle Dateien erzeugen.
Kritisch werden zusätzlich Datei-Berechtigungen, Eigentümer-IDs und erweiterte Attribute wie xattrs, die je nach Host-Betriebssystem und Docker-Version unterschiedlich gesetzt werden können. Für reproduzierbare Docker Builds empfiehlt sich, COPY --chown und explizite chmod-Aufrufe im Dockerfile konsequent zu nutzen, statt sich auf Host-Standardwerte zu verlassen, die zwischen Entwickler-Laptop und CI-Runner variieren können.
7. Reproduzierbarkeit verifizieren: Rebuild und Digest-Vergleich
Die einzige verlässliche Methode, reproduzierbare Docker Builds tatsächlich zu bestätigen, ist der wiederholte Build auf unterschiedlicher Infrastruktur mit anschließendem Digest-Vergleich. Ein Build auf dem lokalen Entwicklungslaptop und ein zweiter Build desselben Commits auf einem unabhängigen CI-Runner müssen denselben Manifest-Digest liefern, wenn der Build wirklich deterministisch ist.
Das Projekt rebuilderd, ursprünglich für reproduzierbare Linux-Distributionspakete entwickelt, lässt sich mit Anpassungen auch für kontinuierliche Rebuild-Verifikation von Container-Images nutzen: Ein unabhängiger Rebuilder baut regelmäßig dasselbe Image aus demselben Quellcode nach und meldet jede Abweichung im Digest als potenziellen Determinismus-Bug oder als Hinweis auf eine kompromittierte 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. Reproduzierbare Builds in der CI-Pipeline erzwingen
In der CI-Pipeline lohnt sich ein dedizierter Verifikations-Job, der nach dem regulären Build einen zweiten, unabhängigen Rebuild anstößt und die Digests vergleicht, bevor ein Image als freigegeben markiert wird. Dieser zusätzliche Schritt kostet Build-Zeit, liefert aber einen belastbaren Nachweis, dass reproduzierbare Docker Builds tatsächlich funktionieren und nicht nur theoretisch möglich wären.
Kombiniert mit Image Signing aus Cosign und einer SBOM aus Syft entsteht eine vollständige Nachweiskette: Der Digest ist reproduzierbar aus dem Quellcode ableitbar, die Signatur beweist die Herkunft aus der eigenen Pipeline, und die SBOM dokumentiert den exakten Inhalt. Für regulierte Branchen ist diese Kombination zunehmend eine explizite Anforderung, nicht mehr nur eine 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. Deterministische vs. nicht-deterministische Build-Elemente
Nicht jeder Bestandteil eines Docker-Builds ist gleich anfällig für Nicht-Determinismus. Die folgende Tabelle ordnet die häufigsten Ursachen für abweichende Digests danach ein, wie leicht sie sich beheben lassen.
| Build-Element | Nicht-deterministisch, wenn | Deterministische Lösung |
|---|---|---|
| Layer-Zeitstempel | Systemzeit zur Build-Zeit | SOURCE_DATE_EPOCH mit rewrite-timestamp |
| Basis-Image | Referenz per Tag | Referenz per Digest |
| Abhängigkeitsversionen | Ohne Lockfile installiert | composer.lock / package-lock.json strikt nutzen |
| Dateireihenfolge im Layer | Externe Tools außerhalb BuildKit-Kontrolle | BuildKit-Standardsortierung nach Pfadname |
| Parallele RUN-Schritte | Nebenläufige Dateisystem-Konflikte | Kritische Schritte explizit serialisieren |
Die meisten dieser Ursachen lassen sich mit überschaubarem Aufwand beheben, sobald sie einmal identifiziert sind. Der größte praktische Hebel bleibt SOURCE_DATE_EPOCH in Kombination mit Digest-Pinning der Basis-Images, weil diese beiden Maßnahmen bereits den überwiegenden Teil des typischen Nicht-Determinismus in Docker-Builds beseitigen.
Mironsoft
Deterministische Build-Pipelines und Supply-Chain-Nachweise
Reproduzierbare Docker Builds für eure Pipeline?
Wir analysieren eure Dockerfiles auf Nicht-Determinismus, richten SOURCE_DATE_EPOCH und Digest-Pinning ein und ergänzen einen Verifikations-Job, der reproduzierbare Builds nachweisbar in der CI-Pipeline bestätigt.
Determinismus-Audit
Dockerfiles auf Zeitstempel, Tag-Referenzen und Nichtdeterminismus prüfen
SOURCE_DATE_EPOCH-Setup
BuildKit-Konfiguration für deterministische Layer und Manifeste einrichten
CI-Verifikation
Automatisierten Rebuild-und-Digest-Vergleich in die Pipeline integrieren
10. Zusammenfassung
Reproduzierbare Docker Builds mit SOURCE_DATE_EPOCH lösen ein grundlegendes Vertrauensproblem: den Beweis, dass ein produktiv laufendes Image tatsächlich aus einem bestimmten, geprüften Quellcode-Stand stammt, ohne blindes Vertrauen in die Build-Infrastruktur vorauszusetzen. SOURCE_DATE_EPOCH eliminiert die größte einzelne Ursache für abweichende Digests, indem es den aktuellen Systemzeitpunkt durch einen aus dem Commit abgeleiteten, festen Zeitstempel ersetzt.
In Kombination mit Digest-basiertem Pinning der Basis-Images, strikten Lockfiles für Abhängigkeiten und deterministischer BuildKit-Konfiguration lassen sich reproduzierbare Docker Builds in der Praxis zuverlässig erreichen. Die Verifikation über einen unabhängigen Rebuild mit Digest-Vergleich macht aus der theoretischen Möglichkeit einen belastbaren, automatisiert überprüfbaren Nachweis in der CI-Pipeline.
Reproduzierbare Docker Builds mit SOURCE_DATE_EPOCH — Das Wichtigste auf einen Blick
SOURCE_DATE_EPOCH
Aus dem Git-Commit abgeleiteter fester Zeitstempel statt aktueller Systemzeit, verhindert Zeitstempel-Rauschen in Layern.
Digest-Pinning
Basis-Images per @sha256: statt per Tag referenzieren, verhindert unbemerkten Basis-Wechsel.
Lockfiles
composer.lock und package-lock.json strikt nutzen, fixiert exakte Abhängigkeitsversionen inklusive transitiver Pakete.
Verifikation
Unabhängiger Rebuild mit Digest-Vergleich als CI-Job bestätigt tatsächliche Reproduzierbarkeit.