Docker: Build-Cache greift nie – falsche Reihenfolge im Dockerfile
Jeder Docker-Build installiert alle Pakete neu? Dann steht COPY . . zu früh. So ordnest du die Anweisungen, damit der Build-Cache endlich greift.
Aufwand:
ca. 20 Min.
Voraussetzung:
Docker, Editor, Terminal
Das Symptom
Ein Tippfehler im Quellcode, ein neuer Build – und Docker installiert seelenruhig
wieder sämtliche Abhängigkeiten. Jedes Mal. Aus einem Build, der zehn Sekunden
dauern könnte, werden vier Minuten, in der CI-Pipeline entsprechend mehr. Im
Build-Log taucht CACHED nur bei den ersten ein, zwei Zeilen auf, danach läuft
alles wieder von vorn:
=> [2/5] WORKDIR /app CACHED => [3/5] COPY . /app => [4/5] RUN npm ci 92.4s
Die „Layer 8“-Wahrheit
Der Cache ist nicht defekt, er wird bei jedem Build sauber und regelkonform
weggeworfen – von dir. Ein Image besteht aus Layern, und die Docker-Dokumentation
beschreibt die entscheidende Eigenschaft in einem Satz: Ändert sich ein Layer,
sind alle Layer betroffen, die danach kommen. Ein COPY . . weit oben im
Dockerfile zieht damit jede noch so kleine Codeänderung in einen Layer, auf dem
alles Weitere aufbaut. Das nachfolgende npm ci oder pip install hat gar keine
Chance, jemals aus dem Cache zu kommen – seine Grundlage ist bei jedem Build eine
andere.
Die Reihenfolge im Dockerfile ist deshalb keine Stilfrage, sondern die eigentliche
Konfiguration des Cache-Verhaltens. Die Dokumentation nennt die Regel unumwunden:
Teure Schritte gehören nahe an den Anfang, häufig wechselnde ans Ende. Übersetzt
in den Alltag heißt das: zuerst nur package.json und package-lock.json
kopieren, dann installieren, und erst ganz zum Schluss den Quellcode, der sich
mehrmals täglich ändert. Danach überlebt der Installations-Layer hunderte
Commits, weil die Manifeste eben nicht bei jeder Codezeile mitwandern.
Dazu gehört die zweite, gern vergessene Hälfte: eine .dockerignore. Ohne sie
schaufelt COPY . . auch .git, lokale node_modules und Logdateien in den
Layer – Ballast, der den Kontext aufbläht und den Cache aus Gründen entwertet,
die mit deiner Anwendung nichts zu tun haben. Beides zusammen, richtige
Reihenfolge plus Ignore-Datei, verwandelt den minutenlangen Neubau in ein paar
Sekunden. Es ist eine der wenigen Optimierungen, die nichts kostet außer fünf
Minuten Umsortieren.
Die echte, verifizierte Lösung
1000 % faktenbasiert. Jeder Befehl ist real, jeder Pfad existiert. Schritt für Schritt:
1.
Im Build-Log nachsehen, wo der Cache abreißt
Nicht raten, ablesen. Bei einem zweiten Build markiert Docker jeden wiederverwendeten Schritt als CACHED. Der erste Schritt ohne diese Markierung ist die Bruchstelle – und wenn das direkt das COPY der Quellen ist, hast du die Diagnose bereits. Die ausführliche Ausgabe zeigt jeden Schritt einzeln.
docker build --progress=plain -t meineapp .
2.
Das Dockerfile umsortieren: erst das Manifest, dann installieren, dann der Rest
Das ist der eigentliche Eingriff und meist eine Sache von drei Zeilen. Zuerst nur die Dateien der Paketverwaltung kopieren, dann die Abhängigkeiten installieren, erst danach den Quellcode. Solange sich die Manifestdateien nicht ändern, bleibt der teure Installationsschritt im Cache – egal wie oft du am Code schraubst.
FROM node:lts-alpine
WORKDIR /app
# 1. Nur die Manifeste - ändern sich selten
COPY package.json package-lock.json ./
# 2. Teurer Schritt, bleibt dadurch im Cache
RUN npm ci
# 3. Der Quellcode - ändert sich ständig, deshalb ganz unten
COPY . .
CMD ["node", "server.js"]
3.
Dasselbe Muster für Python, PHP oder Go anwenden
Die Sprache ist austauschbar, das Prinzip nicht: Es gibt immer eine Handvoll Dateien, die die Abhängigkeiten beschreiben, und sehr viele, die den Code enthalten. Kopiere die erste Gruppe allein, installiere, kopiere dann die zweite. Bei Python sind das requirements.txt oder pyproject.toml, bei PHP composer.json und composer.lock, bei Go go.mod und go.sum.
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
4.
Eine .dockerignore-Datei anlegen
Ohne sie wandern .git, lokale Abhängigkeitsordner, Logs und Build-Artefakte in den Build-Kontext und damit in den COPY-Layer. Jede Änderung daran – auch eine völlig belanglose – kann den Cache ab dieser Stelle entwerten. Die Datei gehört ins Wurzelverzeichnis des Build-Kontexts und funktioniert wie eine .gitignore.
.git
node_modules
__pycache__
*.log
dist
5.
Den Erfolg messen, nicht vermuten
Einmal bauen, eine einzelne Quelldatei ändern, erneut bauen. Beim zweiten Durchlauf müssen alle Schritte bis einschließlich der Installation als CACHED durchlaufen, und nur das COPY der Quellen sowie alles danach neu ausgeführt werden. Passiert das nicht, steht noch eine häufig wechselnde Anweisung zu weit oben.
time docker build -t meineapp .
touch src/index.js
time docker build -t meineapp .
6.
Aufräumen, wenn der Cache über die Jahre wuchert
Ein greifender Cache belegt Platz – das ist der Preis und normalerweise ein guter. Wird er zu groß, entfernt der Builder-Aufräumbefehl die ungenutzten Einträge. Der nächste Build dauert dann einmalig wieder lange, danach greift der Cache wie zuvor.
docker builder prune
Häufige Fragen (FAQ)
?Warum wirft eine winzige Codeänderung den ganzen Paket-Layer weg?
Weil Docker-Layer aufeinander aufbauen und nicht unabhängig voneinander sind. Die Docker-Dokumentation formuliert es knapp: Ändert sich ein Layer, sind alle Layer betroffen, die danach kommen. Steht COPY . . vor dem Installieren der Abhängigkeiten, dann ändert jede geänderte Quelldatei diesen Layer – und alles danach, also auch das Installieren, muss neu laufen. Der Cache ist nicht kaputt, er wird nur systematisch entwertet. Die Anweisung, die sich am häufigsten ändert, steht schlicht an der falschen Stelle.
?In welcher Reihenfolge gehören die Anweisungen dann?
Teuer und selten geändert nach oben, billig und ständig geändert nach unten. Die Docker-Dokumentation gibt genau diese Faustregel aus: Weil eine Änderung alle folgenden Schritte neu baut, sollen teure Schritte nahe an den Anfang des Dockerfiles und häufig wechselnde Schritte ans Ende. Für eine Anwendung heißt das konkret: erst die Dateien der Paketverwaltung kopieren, dann die Abhängigkeiten installieren, ganz zum Schluss den Quellcode, der sich am häufigsten ändert. Dann bleibt der Installations-Layer über hunderte Code-Commits hinweg im Cache.
?Wozu noch eine .dockerignore-Datei, wenn die Reihenfolge stimmt?
Weil der Build-Kontext alles ist, was der Client an den Builder schickt – bei einem unbedachten COPY . . landet der ganze Ordner in dem Layer, inklusive .git, lokaler node_modules, Log- und Build-Verzeichnissen. Die Docker-Dokumentation empfiehlt dafür eine .dockerignore-Datei im Wurzelverzeichnis des Build-Kontexts; sie funktioniert ähnlich wie eine .gitignore und schließt Dateien und Verzeichnisse aus dem Kontext aus. Der Effekt ist doppelt: Der Kontext wird kleiner, und lokale Nebensächlichkeiten können den Cache nicht mehr durch bloße Anwesenheit invalidieren.
Belege
Diese Seite ist gegen die folgenden Primärquellen geprüft – Hersteller, Standard oder Gesetzestext. Wer es nicht glaubt, muss es nicht: Da steht, wo es nachzulesen ist.
Docker bricht mit 'no space left on device' ab, obwohl scheinbar Platz da ist? Alte Images, Container und Build-Cache fressen die Platte. So räumst du auf.
SWLästigpermission denied while trying to connect to the Docker daemon socket