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

Docker-Image verkleinern: Multi-Stage- und Alpine-Leitfaden

Ein Docker-Image verkleinern wirkt sich direkt auf Deploy-Geschwindigkeit und Sicherheit aus. Ein mehrere Gigabyte großes Image verlängert Build-Zeiten, erhöht Registry-Kosten, verlangsamt Deploys und vergrößert die Angriffsfläche, weil es Pakete mitliefert, die Sie nie nutzen. Die gute Nachricht: In den meisten Projekten lässt sich die Imagegröße mit einer Handvoll Kerntechniken um 70-80 % senken. In diesem Leitfaden behandle ich Multi-Stage-Builds, Alpine-basierte Images, .dockerignore und Layer-Optimierung anhand praktischer Beispiele.

Warum ist die Imagegröße wichtig?

Für ein aufgeblähtes Image zahlen Sie in jeder Phase. Große Images brauchen länger zum Pushen in die Registry, lassen Sie länger in der CI/CD-Pipeline warten und blähen den Traffic beim Pull in die Produktion auf. In Autoscaling-Umgebungen lädt jeder neue Pod oder Container das Image von Grund auf herunter; der Unterschied zwischen 50 MB und 1,2 GB summiert sich über hunderte Container zu Minuten.

  • Geschwindigkeit: Ein kleineres Image bedeutet schnellere Pulls, Pushes und Cold Starts.
  • Sicherheit: Weniger Pakete bedeuten weniger CVEs und eine kleinere Angriffsfläche.
  • Kosten: Registry-Speicher und Netzwerk-Traffic skalieren direkt mit der Größe.

Das richtige Base-Image wählen

Der einfachste Gewinn kommt aus der Wahl des Base-Images. Die meisten offiziellen Sprach-Images basieren auf einem vollständigen Debian und enthalten Build-Tools, Dokumentation und jede Erweiterung der Sprache. In den meisten Fällen brauchen Sie nur die Runtime.

  • alpine: Eine winzige, auf musl libc basierende Distribution von ~5 MB. Varianten wie node:20-alpine oder python:3.12-alpine sind nur ein Bruchteil der vollen Version.
  • slim: Varianten wie python:3.12-slim sind Debian-basiert, aber von unnötigen Paketen befreit. Ein guter Mittelweg, wenn Sie das musl-Kompatibilitätsrisiko von Alpine vermeiden wollen.
  • distroless: Googles gcr.io/distroless-Images enthalten nur Ihre App und die Runtime; es gibt nicht einmal eine Shell. Eine der kleinsten und sichersten Optionen für die Produktion.

Ein Hinweis: Da Alpine musl libc verwendet, kann es Build-Probleme mit nativen Erweiterungen verursachen, die von glibc abhängen (insbesondere Pakete mit C-Erweiterungen in Python wie numpy oder pandas). In solchen Fällen läuft slim reibungsloser.

Build und Runtime mit Multi-Stage-Builds trennen

Die mächtigste Technik zum Verkleinern von Images ist der Multi-Stage-Build. Die Idee ist einfach: Sie kompilieren die App in einer „Builder“-Stufe mit allen Build-Tools und kopieren dann nur das erzeugte Ergebnis in eine saubere, kleine finale Stufe. Compiler, Dev-Abhängigkeiten und Zwischendateien gelangen nie ins finale Image.

Ein typisches Beispiel für eine Node.js-App:

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

FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]

Hier belässt npm ci --omit=dev nur die Produktionsabhängigkeiten im finalen Image, während COPY --from=builder nur den kompilierten dist-Ordner überträgt. Alles Build-Spezifische bleibt in der ersten Stufe und wird verworfen.

Bei kompilierten Sprachen ist der Gewinn noch größer. Das Go-Beispiel zeigt die Stärke, eine statische Binary zu erzeugen und auf ein Scratch-Image zu legen:

FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server ./cmd/server

FROM gcr.io/distroless/static-debian12
COPY --from=build /app/server /server
ENTRYPOINT ["/server"]

Mit diesem Ansatz schrumpft eine Build-Umgebung von mehreren hundert Megabyte auf eine einzige Binary von wenigen Megabyte.

Anzahl und Inhalt der Layer optimieren

Jeder RUN-, COPY- und ADD-Befehl erzeugt einen neuen Layer, und in einem Layer hinzugefügte Daten bleiben in der Imagehistorie, selbst wenn ein späterer Layer sie löscht. Deshalb muss das Aufräumen im selben RUN erfolgen:

RUN apt-get update \
 && apt-get install -y --no-install-recommends curl ca-certificates \
 && rm -rf /var/lib/apt/lists/*

Hier überspringt --no-install-recommends empfohlene, aber unnötige Pakete, während rm -rf /var/lib/apt/lists/* den apt-Cache im selben Layer leert. Verteilen Sie dies auf separate Zeilen, bleibt der Cache im Image.

Ein weiterer entscheidender Punkt für das Layer-Caching ist die Befehlsreihenfolge: Kopieren Sie zuerst, was sich selten ändert (Abhängigkeitsmanifeste), und danach, was sich oft ändert (Quellcode). COPY package*.json ./ vor COPY . . zu platzieren, sorgt dafür, dass npm ci nicht bei jeder Codeänderung erneut läuft.

Unnötiges mit .dockerignore ausschließen

Jede Datei, die an den Build-Kontext gesendet wird, kann ins Image gelangen und den Build verlangsamen. Die .dockerignore-Datei funktioniert wie .gitignore und liefert oft einen der schnellsten Gewinne:

.git
node_modules
dist
*.log
.env
.env.*
coverage
Dockerfile
docker-compose.yml
README.md

Besonders das Ausschließen von node_modules und .git verhindert sowohl einen aufgeblähten Build-Kontext als auch das versehentliche Kopieren großer Verzeichnisse. Auch das Ausschließen von .env-Dateien ist aus Sicherheitsgründen unerlässlich.

Größe messen und analysieren

Messen Sie beim Optimieren, um zu sehen, was wirklich wirkt. Der Befehl docker images liefert einen schnellen Vergleich:

docker images myapp --format "{{.Repository}}:{{.Tag}} {{.Size}}"

Für eine tiefere Analyse ist das Tool dive unverzichtbar; es zeigt die Größe jedes Layers und welche Dateien hinzugefügt wurden, sodass Sie die Layer aufspüren, die der „Effizienz“ schaden. BuildKit aktiviert zu lassen (DOCKER_BUILDKIT=1, oder Standard im modernen Docker) ermöglicht zudem parallele und effizientere Builds. Um den Build-Cache klüger zu nutzen, können Sie mit RUN --mount=type=cache einen persistenten Abhängigkeits-Cache vorhalten.

Häufig gestellte Fragen

Ist Alpine immer die beste Wahl?

Nein. Alpine ist sehr klein, aber da es musl libc verwendet, können bei manchen nativen Paketen Build-Probleme und, selten, Unterschiede im DNS-Verhalten auftreten. Haben Sie ein Python/Node-Projekt mit vielen nativen Abhängigkeiten, läuft die slim-Variante meist reibungsloser und ist immer noch deutlich kleiner als das volle Image.

Verlangsamen Multi-Stage-Builds die CI/CD?

In der Praxis nicht. Die Builder-Stufe wird gecacht und läuft nur erneut, wenn sich Abhängigkeiten oder Quellcode ändern. Dank des kleinen Ergebnis-Images verkürzen sich Push- und Pull-Zeiten; insgesamt wird die Pipeline meist schneller.

Ich habe das Image verkleinert, aber es ist noch groß, was kann ich noch tun?

Finden Sie mit dive die größten Layer. Der Übeltäter ist meist ein nicht geleerter apt/npm-Cache, unnötige Dev-Abhängigkeiten, node_modules, das in den Build-Kontext gelangt, oder große kopierte statische Dateien. Die finale Stufe auf distroless oder scratch umzustellen bringt zusätzlichen Gewinn.

Verschlingen Ihre Images noch immer Gigabyte? Wenn Sie Hilfe brauchen, Ihr Dockerfile auf Multi-Stage umzustellen und Ihre CI/CD-Pipeline zu beschleunigen, kontaktieren Sie mich.

Bu kategorideki tüm yazılar →

Devamı için