Ein Docker-Image, das mehrere hundert Megabyte oder gar mehrere Gigabyte groß ist, kostet dich an jeder Stelle deiner Pipeline Zeit und Geld: längere Build-Zeiten, langsamere Pushes in die Registry, träge Deployments und eine größere Angriffsfläche. Dabei ist ein aufgeblähtes Image fast nie nötig. Mit ein paar gezielten Techniken bekommst du dasselbe Programm in einem Bruchteil der Größe lauffähig. Dieser Artikel zeigt dir die wichtigsten Hebel – von der Wahl des Base-Images bis zu Multi-Stage-Builds und Distroless-Containern.
Warum die Image-Größe überhaupt zählt
Bevor wir optimieren, lohnt sich der Blick darauf, warum kleine Images konkret besser sind:
- Schnellere Deployments: Jeder Node, auf dem ein Container startet, muss das Image ziehen. Ein 50-MB-Image ist in Sekunden da, ein 1,5-GB-Image lässt deine Pods minutenlang im Status ImagePullBackOff hängen.
- Geringere Kosten: Registry-Speicher, Egress-Traffic und Build-Minuten in der CI werden bei vielen Anbietern abgerechnet. Kleinere Images sind direkt günstiger.
- Weniger Angriffsfläche: Jedes Paket, jede Shell und jedes Tool im Image ist eine potenzielle Schwachstelle. Was nicht drin ist, kann nicht ausgenutzt werden.
- Schnellere Builds: Wer Layer-Caching richtig nutzt, baut nur das neu, was sich geändert hat.
Das richtige Base-Image wählen
Die größte einzelne Stellschraube ist das Base-Image. Wer mit FROM ubuntu oder FROM node (ohne Suffix) startet, schleppt ein komplettes Betriebssystem mit hunderten Paketen mit, von denen die Anwendung die wenigsten braucht.
Alpine als schlanke Standardwahl
Alpine Linux ist eine minimalistische Distribution, deren Base-Image nur etwa 5–8 MB groß ist. Viele offizielle Images bieten eine Alpine-Variante an, etwa node:20-alpine oder python:3.12-alpine. Statt 1 GB landest du oft bei 50–150 MB.
Achtung: Alpine nutzt musl libc statt der weit verbreiteten glibc. Die allermeisten Programme laufen problemlos, doch einige kompilierte Abhängigkeiten (besonders mit nativen Erweiterungen) machen Ärger. Teste deine Anwendung gründlich, bevor du Alpine in Produktion einsetzt.
Slim- und Distroless-Varianten
Wenn Alpine Kompatibilitätsprobleme macht, sind Slim-Images (z. B. python:3.12-slim) ein guter Mittelweg: glibc-basiert, aber ohne unnötige Tools und Doku. Noch radikaler sind Distroless-Images von Google: Sie enthalten nur die Laufzeitumgebung und deine Anwendung – keine Shell, keinen Paketmanager, kein ls. Das ist extrem sicher, erschwert aber das Debugging, weil du dich nicht per docker exec in den Container einloggen kannst.
Multi-Stage-Builds: Build-Werkzeuge draußen lassen
Der wirkungsvollste Trick für kompilierte Sprachen und Frontends ist der Multi-Stage-Build. Die Idee: In einer ersten Stage baust du die Anwendung mit allen Compilern, Build-Tools und Dev-Abhängigkeiten. In einer zweiten, schlanken Stage kopierst du nur das fertige Artefakt herüber. Alles, was nur zum Bauen nötig war, bleibt zurück.
Ein Beispiel für eine Node-Anwendung:
- Stage 1 (Builder):
FROM node:20 AS builder, installiert alle Abhängigkeiten und führtnpm run buildaus. - Stage 2 (Runtime):
FROM node:20-alpine, kopiert mitCOPY --from=builder /app/dist ./distnur das Build-Ergebnis und installiert ausschließlich die Produktionsabhängigkeiten.
Das finale Image enthält weder den TypeScript-Compiler noch die Dev-Dependencies noch die Quelldateien. Bei einer typischen React- oder Vue-App schrumpft das Image so von über 1 GB auf unter 100 MB.
Bei Go oder Rust geht es noch weiter: Du baust eine statisch gelinkte Binärdatei und kopierst sie in ein scratch-Image (das komplett leere Base-Image). Das Ergebnis sind oft Images von unter 20 MB, die nur die eine Binärdatei enthalten.
Layer-Caching clever nutzen
Docker baut Images schichtweise. Jede Anweisung im Dockerfile erzeugt einen Layer, und Docker cached diese Layer. Ändert sich ein Layer, müssen alle darunterliegenden neu gebaut werden. Daraus folgt eine wichtige Regel: Ordne die Anweisungen vom seltensten zum häufigsten Wechsel.
Klassisches Beispiel bei Node: Kopiere zuerst nur package.json und package-lock.json und führe npm ci aus, bevor du den restlichen Quellcode kopierst. So bleibt der teure Installations-Layer im Cache, solange sich deine Abhängigkeiten nicht ändern – auch wenn du tausendmal am Code feilst.
Vermeide außerdem das häufige Anti-Pattern, mehrere RUN-Befehle für zusammengehörige Schritte zu nutzen. Fasse Paketinstallation und Aufräumen in einem einzigen RUN zusammen und lösche Caches im selben Schritt:
- Bei Debian/Ubuntu:
apt-get update && apt-get install -y --no-install-recommends paket && rm -rf /var/lib/apt/lists/* - Bei Alpine:
apk add --no-cache paket
Wenn du das Aufräumen in einen separaten RUN packst, bleiben die gelöschten Dateien im vorherigen Layer erhalten und das Image bleibt groß – Layer sind additiv, ein späteres Löschen entfernt nichts aus früheren Schichten.
.dockerignore nicht vergessen
Eine oft übersehene, aber simple Maßnahme ist die Datei .dockerignore. Sie funktioniert wie .gitignore und verhindert, dass unnötige Dateien in den Build-Kontext und damit ins Image gelangen. Typische Kandidaten:
node_modules(wird im Container ohnehin frisch installiert).gitmit der kompletten Repo-Historie- Log-Dateien, lokale Umgebungsdateien wie
.env, Test-Coverage-Reports - Build-Artefakte und temporäre Verzeichnisse
Das spart nicht nur Größe, sondern beschleunigt auch jeden Build, weil Docker weniger Daten zum Daemon übertragen muss. Wenn du ein verstecktes .env-File aus dem Build heraushalten willst, ist .dockerignore auch ein Sicherheitsgewinn – sonst landen womöglich Secrets in deinem Image.
Größe analysieren und messen
Optimieren ohne Messen ist Raten. Mit docker images siehst du die Gesamtgröße, mit docker history <image> bekommst du die Größe jedes einzelnen Layers aufgeschlüsselt. So erkennst du sofort den Layer, der die meisten Megabyte beisteuert.
Noch komfortabler ist das Open-Source-Tool dive, das dir interaktiv durch die Layer navigiert und sogar einen "Efficiency Score" anzeigt, der verschwendeten Speicher aufdeckt. Wenn du mit langen Hash-Werten oder Base64-codierten Tokens arbeitest, hilft dir nebenbei unser Webtools-Bereich mit praktischen Konvertern und Generatoren beim Aufbereiten von Konfigurationswerten.
Sicherheit als willkommener Nebeneffekt
Kleine Images sind fast immer auch sicherere Images. Weniger Pakete bedeuten weniger CVEs (bekannte Schwachstellen). Scanne deine Images regelmäßig mit Tools wie Trivy oder Grype, um Schwachstellen in den enthaltenen Paketen zu finden. Setze zudem einen nicht-root-Benutzer im Container, indem du mit USER appuser arbeitest – läuft der Prozess als root und bricht jemand aus, hat er sofort Root-Rechte im Host-Kontext.
Eine pragmatische Reihenfolge
Wenn du ein bestehendes, zu großes Image optimieren willst, gehe in dieser Reihenfolge vor:
- Lege ein sinnvolles
.dockerignorean – schnellster Gewinn ohne Risiko. - Wechsle auf eine
slim- oderalpine-Variante des Base-Images und teste gründlich. - Führe einen Multi-Stage-Build ein, um Build-Tools aus dem finalen Image zu verbannen.
- Optimiere die Layer-Reihenfolge für besseres Caching.
- Räume Caches im selben
RUNauf und fasse Befehle zusammen. - Erwäge Distroless oder
scratchfür maximale Schlankheit bei kompilierten Sprachen.
Häufige Fragen
Macht ein kleineres Image meine Anwendung schneller?
Nicht zur Laufzeit – ein laufender Container verhält sich gleich. Schneller wird der Start, weil das Image schneller heruntergeladen wird, und die gesamte CI/CD-Pipeline, weil weniger Daten bewegt werden. Bei Autoscaling, wo ständig neue Container hochfahren, ist das ein spürbarer Unterschied.
Ist Alpine immer die beste Wahl?
Nein. Alpine ist sehr klein, aber die musl-libc kann zu schwer auffindbaren Bugs bei nativen Abhängigkeiten führen, und manchmal sind Builds wegen Quellkompilierung sogar langsamer. Wenn du Probleme hast, ist ein slim-Image oft die unkompliziertere Wahl mit fast genauso gutem Größenvorteil.
Wie debugge ich ein Distroless- oder scratch-Image ohne Shell?
Du kannst während der Entwicklung eine Debug-Variante mit Shell nutzen (Google bietet :debug-Tags an) oder mit neueren Docker-Versionen docker debug beziehungsweise ephemere Debug-Container über kubectl debug einsetzen, die ein eigenes Toolset mitbringen, ohne das Produktions-Image zu verändern.
Lohnt sich der Aufwand für kleine Projekte?
Ein .dockerignore und ein slim-Base-Image kosten dich fünf Minuten und sind immer sinnvoll. Den vollen Multi-Stage-Aufwand betreibst du dort, wo es zählt: bei Images, die häufig gebaut, gepusht und deployed werden, oder die in großen Stückzahlen laufen.