aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Tools & DevOps

Dockerfile schreiben: effiziente Images mit Layern und Cache

Ein gut geschriebenes Dockerfile ist das Herzstück des Rezepts, das deine App überall gleich laufen lässt. Dennoch erzeugen die meisten Dockerfiles Images, die größer sind als nötig, und bauen bei der kleinsten Änderung alles neu. In diesem Leitfaden gehen wir Dockers Modell aus Layern und Cache Schritt für Schritt durch und bauen Images, die kleiner, schneller und reproduzierbar sind.

Das Layer-Modell: jede Anweisung ist ein Layer

Ein Image besteht aus gestapelten, schreibgeschützten Layern. Jede Anweisung in einem Dockerfile — FROM, RUN, COPY, ADD — erzeugt einen neuen Layer. Docker cacht diese Layer: wenn eine Anweisung und ihre Eingaben unverändert sind, berechnet Docker den Layer nicht neu, sondern verwendet den gecachten wieder.

Der entscheidende Punkt: wenn sich ein Layer ändert, wird auch der Cache jedes darunterliegenden Layers ungültig. Häufig wechselnde Dinge ans Ende des Dockerfiles zu setzen und selten wechselnde an den Anfang, wirkt sich also direkt auf die Build-Zeit aus. Die Reihenfolge ist die halbe Miete.

Beginnen wir mit einem einfachen Dockerfile

Ein typisches, aber fehlerhaftes Beispiel für eine Node.js-App sieht so aus:

FROM node:20
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]

Das funktioniert, aber es gibt ein Problem: weil COPY . . das gesamte Projekt kopiert, macht das Ändern einer einzigen Quellcodezeile den Cache des darunterliegenden RUN npm install-Layers ungültig, sodass Abhängigkeiten jedes Mal neu installiert werden. Dabei müssten sich die Abhängigkeiten gar nicht ändern, wenn package.json unverändert ist.

Den Cache richtig nutzen: Abhängigkeiten trennen

Die Lösung besteht darin, zuerst nur die Abhängigkeitsmanifeste zu kopieren und zu installieren und den Anwendungscode bis zuletzt aufzuheben:

FROM node:20
WORKDIR /app

# Zuerst nur die Abhängigkeitsdateien
COPY package.json package-lock.json ./
RUN npm ci

# Dann der Anwendungscode
COPY . .

CMD ["node", "server.js"]

Jetzt läuft nur der letzte COPY . .-Layer erneut, wenn sich der Quellcode ändert; der npm ci-Layer kommt aus dem Cache. Solange sich package-lock.json nicht ändert, werden Abhängigkeiten nicht neu installiert. Diese kleine Umordnung kann die Build-Zeit von Minuten auf Sekunden verkürzen.

npm ci statt npm install zu verwenden, ist ebenfalls eine bewusste Wahl: ci hält sich exakt an die Lock-Datei, ist deterministisch und für CI-Umgebungen zuverlässiger.

Überflüssiges mit .dockerignore aussortieren

COPY . . kopiert alles — node_modules, .git, Log-Dateien, deine lokale .env... Das bläht das Image auf und macht den Cache unnötig ungültig. Füge eine .dockerignore-Datei im Projektstamm hinzu:

node_modules
.git
.gitignore
Dockerfile
*.log
.env
dist
coverage

Diese Datei funktioniert wie .gitignore und listet Pfade auf, die niemals in den Build-Kontext (die an den Docker-Daemon gesendeten Dateien) gelangen sollen. Ein kleinerer Kontext bedeutet schnellere Builds und ein saubereres Image.

Multi-Stage-Builds: die Build-Tools entsorgen

Die Werkzeuge zum Bauen einer App (Compiler, Dev-Abhängigkeiten) werden zum Ausführen nicht benötigt. Ein Multi-Stage-Build erlaubt es, in einer Stage zu bauen und nur das Ergebnis in ein kleines End-Image zu übernehmen:

FROM node:20 AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# Eine schlanke Endstufe, die nur die Produktionsausgabe übernimmt
FROM node:20-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
CMD ["node", "dist/server.js"]

Hier kopiert --from=build nur den kompilierten dist-Ordner in die zweite Stage. Das End-Image enthält keine Build-Tools oder Dev-Abhängigkeiten, was die Image-Größe oft halbiert oder mehr. Die Wahl eines schlanken Basis-Images wie node:20-slim reduziert ebenfalls Größe und Angriffsfläche.

Praktische Regeln und häufige Fehler

  • RUN-Befehle kombinieren: jedes RUN ist ein Layer. Verkette Paketinstallationen mit && und räume im selben Layer auf; etwa auf einer Debian-Basis apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*.
  • Versionen festpinnen: FROM node:20 ist besser als FROM node:latest; nutze für Reproduzierbarkeit eine konkrete Version.
  • CMD und ENTRYPOINT in Exec-Form schreiben: CMD ["node", "server.js"] leitet Signale (SIGTERM) im Vergleich zur Shell-Form korrekt weiter und ermöglicht ein sauberes Herunterfahren.
  • Nicht als root laufen: definiere in der Produktion einen USER, damit der Container als unprivilegierter Benutzer läuft.
  • WORKDIR verwenden: bevorzuge WORKDIR gegenüber cd; es ist lesbar und bleibt über Layer hinweg erhalten.

Das Image bauen und prüfen

Sobald das Dockerfile fertig ist, sind das Bauen des Images und das Prüfen der Größe unkompliziert:

docker build -t meine-app:1.0 .
docker images meine-app
docker history meine-app:1.0

docker history zeigt Layer für Layer, welche Anweisung wie viel Platz belegt; es ist das praktischste Werkzeug, um aufgeblähte Stellen zu finden. Mit BuildKit (Standard im modernen Docker) zeigt die Build-Ausgabe auch, welche Schritte CACHED waren.

Häufige Fragen

Soll ich ein Dockerfile oder Compose verwenden?

Sie erfüllen unterschiedliche Aufgaben. Ein Dockerfile definiert, wie ein einzelnes Image gebaut wird; docker compose definiert und startet mehrere Container zusammen (etwa eine App plus eine Datenbank). Compose referenziert dein Dockerfile meist über build: — das eine ersetzt also nicht das andere, sie werden zusammen verwendet.

Was ist der Unterschied zwischen COPY und ADD?

Beide kopieren Dateien, aber ADD hat zwei zusätzliche Fähigkeiten: das Herunterladen von entfernten URLs und das automatische Entpacken lokaler tar-Archive. Da dieses Verhalten überraschen kann, bevorzuge für einfaches Kopieren immer COPY und nutze ADD nur für Sonderfälle wie das Entpacken eines Tarballs.

Warum ist mein Image immer noch so groß?

Die häufigsten Ursachen: ein nicht-schlankes Basis-Image (probiere slim oder alpine), kein Multi-Stage-Build, kein Aufräumen im selben Layer und ein fehlendes .dockerignore. Prüfe die Layer mit docker history und nimm den größten ins Visier.

Brauchst du ein effizientes, kleines und schnelles Image? Ich kann deine bestehenden Dockerfiles auf Layer und Cache optimieren und deine CI/CD-Pipelines beschleunigen — nimm Kontakt auf.

Devamı için