Reproduzierbare Docker Builds mit SOURCE_DATE_EPOCH
AI generated
FROM
RUN
Docker · Reproducible Builds · BuildKit · Supply Chain
Reproduzierbare Docker Builds mit SOURCE_DATE_EPOCH
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.

17 Min. Lesezeit SOURCE_DATE_EPOCH · BuildKit · Digest-Verifikation Supply Chain Security · Docker 25+

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.

11. FAQ: Reproduzierbare Docker Builds mit SOURCE_DATE_EPOCH

1Was ist ein reproduzierbarer Build?
Erzeugt aus identischem Quellcode bei jedem Durchlauf denselben Image-Digest, unabhängig von Zeitpunkt oder Maschine.
2Was ist SOURCE_DATE_EPOCH?
Ein fester Unix-Zeitstempel, den Build-Werkzeuge statt der aktuellen Systemzeit für eingebettete Zeitangaben nutzen.
3Woher stammt der Wert?
Typischerweise aus dem Commit-Zeitstempel mit git log -1 --format=%ct.
4Was ist rewrite-timestamp?
Setzt alle Dateizeitstempel im Layer nachträglich auf SOURCE_DATE_EPOCH zurück.
5Warum Digest statt Tag?
Tags ändern sich über die Zeit, Digests fixieren das Basis-Image auf unveränderlichen Inhalt.
6Reicht SOURCE_DATE_EPOCH allein?
Nein, zusätzlich sind Digest-Pinning, Lockfiles und deterministische Dateireihenfolge nötig.
7Wie verifiziere ich Reproduzierbarkeit?
Zweiter unabhängiger Rebuild und Digest-Vergleich. Identische Digests bestätigen Determinismus.
8Bezug zu Supply-Chain-Sicherheit?
Ermöglicht unabhängigen Nachweis der Herkunft, ergänzend zu Image Signing und SBOM.
9Warum sind parallele RUNs riskant?
Nebenläufige Ausführung kann bei gemeinsamen Zielverzeichnissen unterschiedliche Reihenfolgen erzeugen.
10Unterstützen alle Tools das?
Viele moderne Tools wie BuildKit, tar und gzip ja, ältere Werkzeuge müssen im Einzelfall geprüft werden.