Docker Image Größe reduzieren ohne kaputte Builds
AI generated
Docker · Image-Optimierung · Multi-Stage · DevOps
Docker Image Größe reduzieren
ohne kaputte Builds

Ein 1,2-GB-Docker-Image ist kein Zeichen von Vollständigkeit, sondern von fehlender Optimierung. Multi-Stage Builds, präzise Layer-Reihenfolge, eine durchdachte .dockerignore und die richtige Wahl der Basis-Images machen den Unterschied zwischen einem schlanken, schnellen CI-Build und einem Deployment-Flaschenhals.

14 Min. Lesezeit Multi-Stage · Alpine · Distroless · .dockerignore · Layer-Cache Docker 25+ · BuildKit

1. Warum Image-Größe wirklich zählt

Die Docker Image Größe ist kein ästhetisches Problem. Große Images verlangsamen jeden Schritt der Deployment-Pipeline: Longer Build-Zeiten, langsamere Registry-Pushes, längere Pull-Zeiten auf Produktionsservern und höhere Speicherkosten in Container Registries. In CI/CD-Systemen, die Images bei jedem Build neu ziehen, summiert sich der Overhead über Wochen zu messbaren Stunden an verschwendeter Pipeline-Zeit.

Sicherheit ist ein zweiter Grund, der oft unterschätzt wird. Ein Image mit unnötigen Build-Tools, Shell-Interpretern und Paketmanager-Caches enthält eine erheblich größere Angriffsfläche als ein minimiertes Laufzeit-Image. Jedes Paket, das nicht im finalen Image ist, kann nicht kompromittiert werden. Die Reduzierung der Docker Image Größe und die Reduzierung der Angriffsfläche sind daher dieselbe Maßnahme mit zwei Begründungen.

Das häufigste Muster für überdimensionierte Images ist das Fehlen einer klaren Trennung zwischen Build-Umgebung und Laufzeitumgebung. Ein Node.js-Image, das alle devDependencies enthält, ein PHP-Image mit Composer, Xdebug und PHPStan oder ein Java-Image mit dem gesamten JDK statt nur dem JRE – alle verursachen Images, die um ein Vielfaches größer sind als nötig. Multi-Stage Builds sind die direkte Lösung für dieses Problem.

2. Image-Inhalt analysieren: Was belegt den Platz?

Bevor man die Docker Image Größe reduziert, muss man wissen, was Platz belegt. Das Tool docker history zeigt alle Layer eines Images mit ihrer Größe und dem Dockerfile-Befehl, der sie erzeugt hat. Damit lässt sich schnell erkennen, welcher RUN-Befehl den größten Layer erzeugt und ob Paketmanager-Caches fälschlicherweise im Image landen. docker image inspect liefert Metadaten, aber keine detaillierte Layer-Analyse.

Für tiefere Analyse gibt es das externe Tool dive, das jeden Layer des Images interaktiv inspiziert und zeigt, welche Dateien hinzugefügt, geändert oder gelöscht wurden. Besonders aufschlussreich ist die Ansicht, die zeigt, welche Dateien zwischen Layern gelöscht wurden, aber trotzdem noch im Image-Speicher vorhanden sind – denn RUN rm -rf /var/cache in einem separaten Layer löscht die Dateien nicht wirklich aus dem Image, sondern fügt nur eine Deletion-Markierung hinzu. Das Verständnis dieses Layer-Mechanismus ist der Schlüssel für effektive Docker Image Größe-Reduzierung.


# Analyse image layers and sizes
docker history my-image:latest --no-trunc --format "table { {.CreatedBy} }\t{ {.Size} }"

# Install dive for interactive layer inspection
# https://github.com/wagoodman/dive
docker run --rm -it \
  -v /var/run/docker.sock:/var/run/docker.sock \
  wagoodman/dive:latest my-image:latest

# Show image total size and layer count
docker image inspect my-image:latest | jq '.[0] | {Size: .Size, Layers: (.RootFS.Layers | length)}'

# Compare image sizes before and after optimisation
docker images --format "table { {.Repository} }:{ {.Tag} }\t{ {.Size} }" | grep my-image

# Find all images sorted by size (largest first)
docker images --format "{ {.Size} }\t{ {.Repository} }:{ {.Tag} }" | sort -rh | head -20

3. Multi-Stage Builds: Build-Tools vom Laufzeit-Image trennen

Multi-Stage Builds sind die effektivste Methode, die Docker Image Größe drastisch zu reduzieren. Das Prinzip ist einfach: Ein erster Stage verwendet ein vollständiges Build-Image mit allen Compiler-, Paketmanager- und Test-Tools. Ein zweiter Stage verwendet ein minimales Laufzeit-Image und kopiert nur die fertigen Artefakte aus dem Build-Stage hinein. Der Build-Stage wird am Ende verworfen – er erscheint nicht im finalen Image. Mit diesem Ansatz schrumpfen PHP-Images von 800 MB auf 120 MB, Node.js-Images von 1,2 GB auf 80 MB.

Ein wichtiger Aspekt von Multi-Stage Builds, der oft übersehen wird: Man kann gezielt einzelne Dateien und Verzeichnisse aus mehreren verschiedenen Stages in den finalen Stage kopieren. Ein Build kann einen Stage für Frontend-Assets (Node.js-basiert) und einen für Backend-Code (PHP-basiert) haben, und der finale Stage holt sich aus beiden die fertigen Ergebnisse. Die --from=stage-name-Syntax in COPY macht das möglich. Stages können auch benannt werden (FROM node:22 AS frontend), was lesbarere Dockerfiles ergibt als numerische Stage-Referenzen.


# Multi-stage Dockerfile — PHP application with frontend assets
# Stage 1: Frontend build (Node.js)
FROM node:22-alpine AS frontend
WORKDIR /build
COPY package.json package-lock.json ./
RUN npm ci --only=production
COPY web/tailwind/ ./web/tailwind/
# Build CSS and JS assets
RUN npm run build

# Stage 2: PHP dependencies (Composer)
FROM composer:2.8 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
# Install only production dependencies — skip dev tools
RUN composer install \
    --no-dev \
    --no-scripts \
    --no-autoloader \
    --prefer-dist \
    --optimize-autoloader

# Stage 3: Final runtime image (minimal)
FROM php:8.4-fpm-alpine AS runtime
RUN apk add --no-cache nginx

WORKDIR /var/www/html
# Copy only compiled assets from frontend stage
COPY --from=frontend /build/web/dist/ ./web/
# Copy only vendor directory from composer stage
COPY --from=vendor /app/vendor ./vendor/
# Copy application code
COPY app/ ./app/
COPY pub/ ./pub/

# No build tools, no node_modules, no composer — final image is minimal
EXPOSE 80 9000
CMD ["php-fpm"]

4. Layer-Reihenfolge und Cache-Effizienz

Die Reihenfolge der Befehle im Dockerfile hat direkten Einfluss auf Build-Geschwindigkeit und Docker Image Größe. Docker-Layer werden gecacht, und ein geänderter Layer invalidiert alle nachfolgenden Layer. Das fundamentale Prinzip lautet: Selten ändernde, große Schichten kommen zuerst, häufig ändernder Code kommt zuletzt. Paketinstallationen ändern sich seltener als Anwendungscode – also: zuerst Systempakete installieren, dann Abhängigkeitsdateien kopieren und installieren, dann den eigentlichen Code kopieren.

Eine häufige Layer-Optimierung: Mehrere RUN-Befehle, die logisch zusammengehören, in eine einzige RUN-Anweisung zusammenfassen. Jeder RUN-Befehl erzeugt einen neuen Layer. Wer Pakete in einem Layer installiert und im nächsten Layer den Cache löscht, hat den Cache zwar gelöscht, aber der ursprüngliche Layer mit dem Cache ist noch im Image. Die Lösung: Installation und Cache-Bereinigung in demselben RUN-Befehl. Mit && verbundene Befehle teilen sich einen Layer und erzeugen kein Cache-Artefakt.

5. .dockerignore richtig konfigurieren

Die .dockerignore-Datei ist eine der einfachsten Maßnahmen zur Reduzierung der Docker Image Größe und wird trotzdem häufig vernachlässigt oder komplett weggelassen. Ohne sie kopiert COPY . . alles in den Build-Kontext: node_modules mit Hunderten von Megabytes, .git-Verzeichnis, lokale Umgebungsdateien, IDE-Konfigurationen, Testdaten und Build-Artefakte aus vorherigen lokalen Builds. Das verlangsamt zunächst den Build-Kontext-Transfer zum Docker-Daemon und kann dann unerwünschte Dateien ins Image bringen.

Eine gute .dockerignore-Datei folgt dem Whitelist-Prinzip: Erst alles ausschließen (**), dann nur das Nötige explizit einschließen (!src/, !composer.json). Diese inverse Strategie ist sicherer als das Aufzählen aller auszuschließenden Pfade, weil neue Verzeichnisse automatisch ausgeschlossen sind und nicht manuell zur Exclude-Liste hinzugefügt werden müssen. Das verhindert versehentliches Einschließen sensibler Dateien wie .env-Dateien oder privater Schlüssel.


# .dockerignore — whitelist approach: exclude everything, include only what's needed
**

# Include application source
!src/
!app/
!pub/

# Include dependency manifests (not the resolved dependencies)
!composer.json
!composer.lock
!package.json
!package-lock.json

# Include configuration files needed at build time
!docker/
!Makefile

# Explicitly exclude secrets even if accidentally matched above
**/.env
**/.env.*
**/secrets/
**/*.key
**/*.pem

# Typical directories that should never be in the build context
# node_modules/         — already excluded by ** but explicit for clarity
# vendor/               — rebuilt in multi-stage
# .git/                 — included in ** exclusion
# var/cache/ var/log/   — runtime artifacts

6. Basis-Image-Wahl: Alpine, Slim, Distroless

Die Wahl des Basis-Images ist die fundamentalste Entscheidung für die Docker Image Größe. Alpine Linux ist mit etwa 5 MB das kleinste allgemeine Linux-Basis-Image und enthält apk als Paketmanager. Es eignet sich für die meisten Anwendungen, hat aber Eigenheiten: statt glibc verwendet Alpine musl libc, was bei einigen nativen Erweiterungen – besonders bei PHP-Extensions, die glibc-Spezifika nutzen – zu subtilen Problemen führen kann. Für PHP ist Alpine inzwischen weitgehend stabil, aber einzelne Erweiterungen müssen aus Source kompiliert werden statt aus vorcompilierten Binaries zu installieren.

Googles Distroless-Images gehen noch weiter: Sie enthalten nur die Laufzeitbibliotheken ohne Shell, ohne Paketmanager und ohne andere Utilities. Das Ergebnis sind Images mit minimaler Angriffsfläche – ein Angreifer, der in den Container gelangt, hat keine Shell, keinen Paketmanager und kein curl, um weitere Tools nachzuladen. Distroless-Images eignen sich am besten für go-Binaries (die gar keine externe Abhängigkeit brauchen) und für Java-Laufzeiten. Für PHP sind sie wegen der Komplexität der Extension-Abhängigkeiten weniger praktikabel. Die -slim-Varianten der offiziellen Docker-Images sind ein guter Kompromiss: Debian-basiert mit reduzierten Paketen, aber vollständiger Shell und apt.

7. Paketmanager-Caches im Build eliminieren

Jeder Paketmanager legt während der Installation einen lokalen Cache an. apt füllt /var/cache/apt/archives, apk füllt /var/cache/apk, pip füllt ~/.cache/pip, npm füllt ~/.npm. Diese Caches sind im Betrieb nutzlos und vergrößern das Image ohne Mehrwert. Die Lösung ist, Installation und Cache-Bereinigung in einem einzigen RUN-Befehl zu kombinieren, damit der Cache nie in einem permanenten Layer landet.

Mit Docker BuildKit gibt es einen eleganteren Weg: Mount-Caches. RUN --mount=type=cache,target=/var/cache/apt apt-get install -y nginx verwendet einen flüchtigen Cache, der zwischen Builds wiederverwendet wird, aber nie im finalen Layer-Stack des Images erscheint. Das gibt die Geschwindigkeitsvorteile des Caches für schnelle Rebuilds, ohne die Docker Image Größe zu erhöhen. Diese Technik ist besonders wertvoll bei Images, die viele System-Pakete installieren oder bei denen die apt-Installation ein häufiger Build-Schritt ist.


# Dockerfile — eliminate package manager caches in every layer

# apt: install and clean in same RUN command (without BuildKit)
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        nginx \
        libpng-dev \
    && rm -rf /var/lib/apt/lists/* \
    && apt-get clean

# apk (Alpine): --no-cache skips the cache entirely
RUN apk add --no-cache \
    nginx \
    php84-fpm \
    php84-pdo_mysql

# BuildKit mount cache: cache reused across builds, never lands in image layer
# syntax=docker/dockerfile:1
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt,sharing=locked \
    apt-get update && apt-get install -y --no-install-recommends nginx

# pip: disable cache entirely
RUN pip install --no-cache-dir -r requirements.txt

# npm: use ci (clean install) and cache with BuildKit
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production

8. BuildKit-Features für noch kleinere Images

Docker BuildKit, seit Docker 23 standardmäßig aktiv, bietet Features, die über klassische Dockerfile-Optimierungen hinausgehen. Secret-Mounts ermöglichen, Credentials während des Builds zu verwenden, ohne sie im Image zu speichern: RUN --mount=type=secret,id=npmrc cat /run/secrets/npmrc > ~/.npmrc && npm ci. Die Secret-Datei ist nur während dieses einen RUN-Schritts zugänglich und erscheint in keinem Layer. Das ist der sichere Weg, private Registry-Authentifizierung in Builds einzubauen, ohne Tokens im Image zu verewigen.

SSH-Mounts ermöglichen, SSH-Agent-Authentifizierung während des Builds zu nutzen – etwa um private Git-Repositories zu klonen, ohne SSH-Keys ins Image zu kopieren. Bind-Mounts erlauben, Host-Verzeichnisse temporär im Build zu mounten, ohne sie zu kopieren. Für sehr fortgeschrittene Docker Image Größe-Optimierung gibt es --squash (experimentell): Es fasst alle Layer in einen einzigen zusammen und eliminiert so alle Spuren gelöschter Dateien aus Zwischenlayern. Effektiver als Squash ist aber konsequentes Multi-Stage-Design – dann entstehen die problematischen Zwischen-Layer erst gar nicht.

9. Image-Strategien im Vergleich

Die verschiedenen Optimierungsstrategien lassen sich kombinieren und haben unterschiedliche Aufwand-Nutzen-Verhältnisse. Wer die Docker Image Größe systematisch reduzieren will, sollte mit den High-Impact-Maßnahmen beginnen.

Strategie Typische Einsparung Aufwand Risiko
Multi-Stage Build 60–85% Reduktion Mittel Niedrig bei korrekter COPY
Alpine statt Debian 200–400 MB Gering musl libc-Kompatibilität
.dockerignore 50–500 MB (node_modules) Sehr gering Sehr niedrig
Cache-Bereinigung 20–100 MB Sehr gering Niedrig
Distroless 30–60 MB vs. Alpine Hoch Kein Shell-Debugging möglich

Die Kombination aus Multi-Stage Build und präziser .dockerignore liefert den größten Effekt mit dem geringsten Risiko. Alpine als Basis-Image ist eine sichere zweite Maßnahme. Die Wahl von Distroless ist eine fortgeschrittene Maßnahme mit hohem Sicherheitsgewinn, aber erfordert, dass das Team auf Shell-basiertes Debugging im Container verzichten kann und alle Debugging-Tools extern vorhält.

Mironsoft

Docker-Optimierung, CI/CD-Beschleunigung und Container-Security

Docker-Images, die schnell bauen und wenig Platz brauchen?

Wir analysieren eure Dockerfiles, identifizieren unnötige Layer und implementieren Multi-Stage Builds sowie BuildKit-Optimierungen – mit messbaren Ergebnissen bei Image-Größe und CI-Build-Zeit.

Dockerfile-Audit

Layer-Analyse mit dive, Identifikation von Cache-Problemen und unnötigen Build-Artefakten

Multi-Stage Refactoring

Build-Tools vom Laufzeit-Image trennen – typisch 60–85% Größenreduktion

BuildKit-Optimierung

Mount-Caches, Secret-Mounts und parallele Build-Stages für maximale CI-Geschwindigkeit

10. Zusammenfassung

Die Docker Image Größe zu reduzieren ist kein Einzel-Eingriff, sondern eine Kombination aus mehreren Strategien: Multi-Stage Builds trennen Build-Werkzeuge vom Laufzeit-Image und liefern typisch 60–85% Größenreduktion. Eine präzise .dockerignore-Datei verhindert, dass node_modules, .git und lokale Entwicklungsdateien in den Build-Kontext gelangen. Layer-Reihenfolge und das Zusammenfassen von Installation und Cache-Bereinigung in einem RUN-Befehl eliminieren unnötige Layer. Alpine als Basis-Image spart gegenüber Debian 200–400 MB ohne signifikante Nachteile für die meisten Anwendungen.

BuildKit-Features wie Mount-Caches und Secret-Mounts runden die Optimierungsstrategie ab: schnellere Builds durch Cache-Wiederverwendung ohne Imagevergrößerung, sichere Credential-Übergabe ohne Token-Leaks in Layern. Das Ergebnis sind Images, die schnell in CI-Pipelines bauen, günstig in Registries gespeichert werden und eine kleinere Angriffsfläche bieten. Analyse-Tools wie docker history und dive helfen dabei, den aktuellen Zustand zu verstehen und den Effekt jeder Optimierung messbar zu machen.

Docker Image Größe reduzieren — Das Wichtigste auf einen Blick

Multi-Stage Builds

Build-Stage mit allen Tools, Runtime-Stage nur mit Artefakten – typisch 60–85% Größenreduktion. Stages benennen für lesbare Dockerfiles.

.dockerignore

Whitelist-Ansatz: erst alles ausschließen, dann nur Nötiges einschließen. Verhindert versehentliches Einschließen von .env und node_modules.

Layer-Optimierung

Installation und Cache-Bereinigung im selben RUN-Befehl. Selten ändernde Schichten zuerst für optimales Layer-Caching in CI.

Basis-Image

Alpine für kleine Images mit Shell-Zugriff. Distroless für maximale Sicherheit ohne Shell. -slim als Kompromiss für Debian-Kompatibilität.

11. FAQ: Docker Image Größe reduzieren

1Größter Hebel zur Image-Größenreduzierung?
Multi-Stage Builds – Build-Tools nie im finalen Image. Ein Refactoring, typisch 60–85% Ersparnis.
2Warum löscht RUN rm Dateien nicht wirklich?
Layer sind additiv. rm in separatem Layer hinterlässt Deletion-Marker, aber vorheriger Layer bleibt. Installation + Bereinigung immer im selben RUN-Befehl.
3Risiken von Alpine als Basis-Image?
musl libc statt glibc – die meisten Extensions funktionieren, einige müssen aus Source kompiliert werden. Tests sicherstellen.
4node_modules im Multi-Stage vermeiden?
Build-Stage: npm installieren + Assets kompilieren. Final-Stage: nur compilierte Assets per COPY --from. node_modules nie im finalen Image.
5.dockerignore Whitelist vs. Blacklist?
Whitelist (** dann !src/) ist sicherer: neue Verzeichnisse automatisch ausgeschlossen. Blacklist muss manuell gepflegt werden.
6Layer-Analyse: welche Tools gibt es?
docker history --no-trunc für schnelle Layer-Übersicht. dive für interaktive Datei-Level-Analyse aller Layer.
7Was sind BuildKit Mount-Caches?
Paketmanager-Caches zwischen Builds wiederverwenden ohne sie in Image-Layer zu speichern. Schnellere Rebuilds ohne Imagevergrößerung.
8Distroless für PHP-Anwendungen?
Nur in fortgeschrittenen Setups. PHP benötigt viele Laufzeitbibliotheken. Alpine oder debian:slim sind praktischer. Distroless ideal für Go-Binaries.
9.env-Dateien vor Image-Einschluss schützen?
In .dockerignore: **/.env und **/.env.*. Beim Whitelist-Ansatz automatisch ausgeschlossen. docker history prüfen für finale Bestätigung.
10Was bewirkt --no-install-recommends?
Installiert nur direkt angeforderte Pakete und zwingend notwendige Abhängigkeiten. Typisch 30–50% weniger Paketgröße als ohne dieses Flag.