Build-Cache-Invalidierung: Warum die Dockerfile-Reihenfolge über Build-Zeiten entscheidet
AI generated
FROM
RUN
Docker · Build-Performance · CI/CD
Build-Cache-Invalidierung im Dockerfile
Warum die Reihenfolge der Befehle über Minuten statt Sekunden entscheidet

Ein schlecht sortiertes Dockerfile kann bei jedem Build sämtliche Layer neu aufbauen, selbst wenn sich nur eine einzige Zeile Anwendungscode geändert hat. Wer versteht, wie Docker sein Layer-Caching anhand der Dockerfile-Reihenfolge invalidiert, kann Build-Zeiten in CI-Pipelines drastisch verkürzen, ohne an der Hardware zu sparen.

16 Min. Lesezeit Layer-Caching COPY-Reihenfolge Build-Zeit-Vergleich

1. Wie Docker Images aus Layern aufbaut

Jede Anweisung in einem Dockerfile, die den Dateisystemzustand verändert, wie RUN, COPY oder ADD, erzeugt beim Build einen eigenen, unveränderlichen Layer. Diese Layer werden übereinandergestapelt und ergeben zusammen das fertige Image. Der entscheidende Vorteil dieses Modells ist, dass Docker jeden Layer einzeln cachen kann, sodass ein erneuter Build nicht zwingend bei null beginnen muss.

Damit dieser Cache genutzt werden kann, prüft Docker vor jedem Layer, ob eine identische Anweisung mit identischem Kontext bereits einmal ausgeführt wurde. Trifft das zu, wird der zwischengespeicherte Layer wiederverwendet, ohne die Anweisung tatsächlich erneut auszuführen. Das spart bei unveränderten Abschnitten wertvolle Zeit, kann aber bei falscher Reihenfolge genau das Gegenteil bewirken.

2. Wie eine einzige Änderung die Kette bricht

Der Cache eines Layers wird ungültig, sobald sich entweder die Anweisung selbst oder, im Fall von COPY und ADD, der Inhalt der kopierten Dateien ändert. Sobald ein Layer invalidiert wird, gilt das automatisch auch für jeden nachfolgenden Layer im Dockerfile, selbst wenn diese späteren Anweisungen inhaltlich völlig unverändert geblieben sind. Docker kann diese Kettenreaktion nicht umgehen, weil jeder Layer auf dem Dateisystemzustand des vorherigen aufbaut.

Genau das macht die Reihenfolge im Dockerfile zum entscheidenden Faktor: Ein Layer, der sich häufig ändert, sollte immer möglichst spät im Dockerfile stehen, während selten wechselnde, aber rechenintensive Schritte wie Paketinstallationen so früh wie möglich platziert werden sollten, um von einem möglichst langen, wiederverwendbaren Cache-Präfix zu profitieren.


# Cache-Kette: Aendert sich Layer 2, sind auch 3 und 4 ungueltig
FROM node:20-alpine     # Layer 1: aendert sich selten
COPY package.json .     # Layer 2: aendert sich gelegentlich
RUN npm install          # Layer 3: haengt von Layer 2 ab
COPY . .                  # Layer 4: aendert sich bei jedem Commit

3. Das häufigste Anti-Pattern: COPY . . zu früh

Der mit Abstand häufigste Fehler ist ein Dockerfile, das den kompletten Quellcode mit einem einzigen COPY . . ganz an den Anfang stellt, gefolgt von der Installation der Abhängigkeiten. Da dieser COPY-Befehl den gesamten Projektinhalt einschließt, ändert sich sein Hash bei praktisch jedem Commit, egal ob sich tatsächlich eine Abhängigkeit geändert hat oder nur eine einzelne Codezeile in einer völlig unabhängigen Datei.

Die Folge ist, dass der nachfolgende Installationsschritt, etwa npm install oder composer install, bei jedem einzelnen Build erneut ausgeführt wird, selbst wenn sich die package.json oder composer.json seit Wochen nicht verändert hat. Bei größeren Projekten mit vielen Abhängigkeiten bedeutet das mehrere Minuten unnötige Wartezeit pro Build, die sich über eine CI-Pipeline mit hoher Build-Frequenz schnell zu erheblichen Kosten summieren.


# Anti-Pattern: Jede Codeaenderung invalidiert npm install
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]

4. Die Lösung: Abhängigkeitsdateien zuerst kopieren

Die etablierte Lösung besteht darin, ausschließlich die Manifestdatei der Abhängigkeiten, also package.json (plus package-lock.json) oder composer.json (plus composer.lock), separat und vor dem restlichen Quellcode zu kopieren. Erst danach folgt der Installationsbefehl, und erst im letzten Schritt wird der restliche Quellcode mit einem eigenen COPY-Befehl hinzugefügt.

Mit dieser Reihenfolge bleibt der teure Installationsschritt so lange im Cache gültig, bis sich tatsächlich eine Abhängigkeit ändert. Reine Codeänderungen, die den überwiegenden Teil der täglichen Commits ausmachen, invalidieren dann ausschließlich den letzten, günstigen COPY-Layer, während der komplette Installationsschritt aus dem Cache wiederverwendet wird.


# Optimiert: npm install bleibt gecacht,
# solange sich package.json/lock nicht aendert
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
CMD ["node", "server.js"]

5. Dasselbe Prinzip für PHP-Projekte mit Composer

Bei PHP-Projekten mit Composer gilt exakt dasselbe Prinzip, nur dass hier composer.json und composer.lock die relevanten Manifestdateien sind. Auch hier lohnt es sich, composer install mit der Option --no-scripts zunächst ohne den restlichen Projektcode auszuführen, sofern die Composer-Scripts selbst nicht zwingend auf den Anwendungscode angewiesen sind, um den Cache-Layer so schlank wie möglich zu halten.

In Magento-Projekten mit vielen Abhängigkeiten macht sich dieser Unterschied besonders deutlich bemerkbar, da composer install dort je nach Anzahl der Module und Netzwerkgeschwindigkeit mehrere Minuten dauern kann. Eine korrekte Layer-Reihenfolge verwandelt diesen Schritt bei reinen Codeänderungen von mehreren Minuten in praktisch null Sekunden.


# Composer-Beispiel mit optimierter Layer-Reihenfolge
FROM php:8.4-fpm
WORKDIR /var/www/html
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader
COPY . .
RUN composer dump-autoload --optimize

6. Konkrete Build-Zeiten im Vergleich

In einem Testprojekt mit rund 40 npm-Abhängigkeiten und einer kleinen Codeänderung in einer einzelnen JavaScript-Datei benötigte das unoptimierte Dockerfile mit vorangestelltem COPY . . bei jedem Build etwa 90 Sekunden, da npm install jedes Mal komplett neu ausgeführt wurde. Nach der Umstellung auf die optimierte Reihenfolge sank die Build-Zeit für denselben Codeänderungsfall auf unter 5 Sekunden, da ausschließlich der letzte COPY-Layer und der finale Build-Schritt neu ausgeführt werden mussten.

Bei einem tatsächlichen Abhängigkeitswechsel, etwa dem Hinzufügen eines neuen npm-Pakets, bleibt die Build-Zeit in beiden Varianten naturgemäß ähnlich hoch, da der Installationsschritt in diesem Fall ohnehin neu ausgeführt werden muss. Der Vorteil der optimierten Reihenfolge zeigt sich also gezielt in der überwiegenden Mehrheit der Alltags-Commits, die ausschließlich Code, aber keine Abhängigkeiten betreffen.

7. Die Rolle von .dockerignore beim Cache-Verhalten

Ein oft übersehener Faktor bei der Cache-Invalidierung ist die Datei .dockerignore. Fehlt sie oder ist sie unvollständig, gelangen Verzeichnisse wie node_modules, .git oder lokale Log-Dateien in den Build-Kontext und damit potenziell in den Hash eines COPY-Befehls, obwohl sie inhaltlich für den Build völlig irrelevant sind und sich zwischen zwei Commits häufig unbeabsichtigt ändern.

Eine sorgfältig gepflegte .dockerignore-Datei reduziert nicht nur die Menge der an den Docker-Daemon übertragenen Daten, sondern stellt auch sicher, dass sich der Hash eines COPY-Layers ausschließlich dann ändert, wenn tatsächlich relevante Quelldateien geändert wurden, was die Vorhersagbarkeit des Cache-Verhaltens deutlich verbessert.


# Typische .dockerignore fuer ein Node/PHP-Projekt
node_modules
vendor
.git
*.log
var/cache
var/log
.env

8. Multi-Stage-Builds als zusätzlicher Hebel

Multi-Stage-Builds verstärken den Effekt einer guten Layer-Reihenfolge zusätzlich, da sich Build-Abhängigkeiten wie Compiler oder Dev-Tools in einer separaten Stage befinden, die nur bei tatsächlicher Codeänderung erneut durchlaufen werden muss, während die finale, schlanke Produktions-Stage lediglich fertige Artefakte übernimmt. In Kombination mit einer optimierten COPY-Reihenfolge lässt sich so sowohl die Build-Zeit als auch die finale Image-Größe reduzieren.

Wichtig ist dabei, dass die Cache-Regeln innerhalb jeder Stage unabhängig gelten, weshalb die Reihenfolge der Anweisungen in jeder einzelnen Stage separat optimiert werden muss und sich eine gute Struktur in der Build-Stage nicht automatisch auf die Runtime-Stage überträgt.


# Multi-Stage-Build mit optimierter Cache-Reihenfolge
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html

9. Checkliste für ein cache-freundliches Dockerfile

Wer ein bestehendes Dockerfile auf Cache-Freundlichkeit überprüfen möchte, sollte systematisch von oben nach unten gehen und sich bei jeder Anweisung fragen, wie häufig sich ihr Inhalt tatsächlich ändert. Anweisungen mit geringer Änderungsfrequenz gehören nach oben, Anweisungen mit hoher Änderungsfrequenz, allen voran das Kopieren des Anwendungscodes selbst, gehören konsequent ans Ende.

Die folgende Tabelle fasst die wichtigsten Prinzipien noch einmal mit ihrer jeweiligen Auswirkung auf die Build-Zeit zusammen, sortiert nach dem in der Praxis beobachteten Effekt auf die Cache-Trefferquote bei alltäglichen Commits.

Prinzip Falsch platziert Richtig platziert Effekt bei reiner Codeänderung
Abhängigkeiten zuerst kopieren COPY . . vor RUN npm install COPY package.json vor RUN npm install Installationsschritt bleibt gecacht
Selten wechselnde Layer nach oben apt-get install nach COPY . apt-get install vor COPY . Paketinstallation bleibt gecacht
.dockerignore pflegen node_modules im Build-Kontext node_modules ausgeschlossen Stabilerer Hash für COPY-Layer
Multi-Stage-Builds nutzen Build-Tools im finalen Image Build-Tools nur in Build-Stage Kleineres Image, unabhängiger Cache
Lock-Dateien mit einbeziehen Nur package.json kopiert package.json und package-lock.json kopiert Reproduzierbare Installation

Mironsoft

Container-Infrastruktur, CI-Pipelines und Deployment-Automatisierung

Docker-Setups, die im Team und in Produktion tragfähig bleiben?

Wir prüfen bestehende Dockerfiles und Compose-Stacks auf Sicherheitslücken, aufgeblähte Images und fragile Build-Pipelines und bauen daraus eine Container-Infrastruktur, die schnell baut, sicher läuft und im Team nachvollziehbar bleibt.

Dockerfile-Review

Multi-Stage-Builds, Layer-Caching und Image-Größe systematisch optimieren.

Security-Audit

Container-Isolation, Secrets-Handling und Image-Scanning gegen echte Angriffsflächen absichern.

CI/CD-Integration

Build-Pipelines, Registries und Deployment-Strategien für reproduzierbare Releases aufbauen.

10. Zusammenfassung

Build-Cache-Invalidierung: Das Wichtigste auf einen Blick

Kernregel

Selten wechselnde Anweisungen zuerst, häufig wechselnde Anweisungen zuletzt im Dockerfile.

Größter Hebel

Manifestdateien wie package.json vor dem restlichen Code kopieren und installieren.

Beobachteter Effekt

Build-Zeit bei reinen Codeänderungen sank in einem Testprojekt von 90 auf unter 5 Sekunden.

Zusatzhebel

.dockerignore pflegen und Multi-Stage-Builds für Build- und Runtime-Trennung nutzen.

11. FAQ: Build-Cache-Invalidierung: Das Wichtigste auf einen Blick

1Was bedeutet Layer-Caching bei Docker konkret?
Jede Anweisung im Dockerfile erzeugt einen eigenen Layer. Docker cacht diese Layer und führt eine Anweisung nur dann erneut aus, wenn sich die Anweisung selbst oder die zugehörigen kopierten Dateien seit dem letzten Build geändert haben.
2Warum sollte package.json vor dem restlichen Quellcode kopiert werden?
Weil sich der Hash eines COPY . . bei praktisch jedem Commit ändert, auch wenn sich keine Abhängigkeit geändert hat. Ein separater COPY-Befehl nur für package.json hält den Installationsschritt so lange im Cache, bis sich tatsächlich eine Abhängigkeit ändert.
3Was passiert, wenn ein Layer im Dockerfile invalidiert wird?
Alle nachfolgenden Layer im Dockerfile werden automatisch ebenfalls invalidiert, selbst wenn ihre eigenen Anweisungen inhaltlich unverändert geblieben sind, da jeder Layer auf dem Dateisystemzustand des vorherigen aufbaut.
4Wie groß kann der Zeitgewinn durch eine optimierte Reihenfolge realistisch sein?
In einem Testprojekt mit rund 40 npm-Abhängigkeiten sank die Build-Zeit bei reinen Codeänderungen von etwa 90 Sekunden auf unter 5 Sekunden, da der teure Installationsschritt aus dem Cache wiederverwendet wurde.
5Gilt das Prinzip auch für composer install bei PHP-Projekten?
Ja, exakt dasselbe Prinzip gilt für composer.json und composer.lock. Auch hier sollte der Installationsschritt vor dem Kopieren des restlichen Anwendungscodes stehen.
6Welche Rolle spielt die .dockerignore-Datei beim Caching?
Eine sorgfältig gepflegte .dockerignore-Datei verhindert, dass irrelevante Dateien wie node_modules oder Logdateien in den Build-Kontext gelangen und dadurch unnötig den Hash eines COPY-Layers verändern.
7Verbessern Multi-Stage-Builds das Cache-Verhalten zusätzlich?
Ja, indem Build-Abhängigkeiten wie Compiler in einer separaten Stage isoliert werden, die unabhängig von der finalen, schlanken Produktions-Stage gecacht wird, was sowohl Build-Zeit als auch Image-Größe reduziert.
8Bleibt der Cache auch bei einem tatsächlichen Abhängigkeitswechsel erhalten?
Nein, ändert sich eine tatsächliche Abhängigkeit in package.json oder composer.json, wird der Installationsschritt regulär neu ausgeführt, unabhängig von der Dockerfile-Reihenfolge, da sich der Hash der kopierten Datei ändert.
9Warum sollten Lock-Dateien wie package-lock.json ebenfalls kopiert werden?
Weil sie die exakten, reproduzierbaren Versionen aller Abhängigkeiten festlegen. Werden sie nicht mitkopiert, kann die Installation im Container von der lokal getesteten Version abweichen.
10Kann eine schlechte Dockerfile-Reihenfolge auch CI-Kosten beeinflussen?
Ja, unnötig lange Build-Zeiten summieren sich bei häufigen Builds in einer CI-Pipeline schnell zu erheblicher Rechenzeit und damit zu messbaren zusätzlichen Kosten, besonders bei nutzungsbasierter Abrechnung.