Docker · Dockerfile · Container Security · DevOps
Dockerfile Reviews: Typische Anti-Patterns
erkennen, verstehen und gezielt beheben

Viele Dockerfiles wachsen organisch und akkumulieren dabei Muster, die Images aufblähen, die Build-Zeit verdoppeln oder die Sicherheit des Containers untergraben. Ein strukturierter Dockerfile-Review deckt diese Anti-Patterns auf – bevor sie in der Produktion zum Problem werden.

18 Min. Lesezeit Anti-Patterns · Multi-Stage · Layer Caching · Security · .dockerignore Docker 24+ · OCI · Linux

1. Warum Dockerfile-Reviews unverzichtbar sind

Ein Dockerfile ist die Blaupause jedes Container-Images – und gleichzeitig eine der am häufigsten vernachlässigten Codedateien in einem Projekt. Während Anwendungscode durch Tests, Code-Reviews und statische Analyse abgesichert wird, landen Dockerfile Anti-Patterns oft unbemerkt in der Produktion. Das Ergebnis: Images, die mehrere Gigabyte groß sind, Build-Zeiten von zehn Minuten, Container, die als Root laufen, und Credentials, die dauerhaft in Image-Layern eingefroren sind.

Ein strukturierter Dockerfile-Review ist keine akademische Übung, sondern hat direkte Auswirkungen auf Build-Geschwindigkeit, Deployment-Häufigkeit und Angriffsfläche. In der Praxis zeigt sich immer wieder dasselbe Muster: Der ursprüngliche Entwickler hat das Dockerfile schnell zusammengestellt, es hat funktioniert und niemand hat es seitdem angefasst. Dabei haben sich zehn bis fünfzehn Dockerfile Anti-Patterns eingeschlichen, die sich beheben ließen, wenn man weiß, wonach man sucht.

Die folgenden Abschnitte gehen durch die häufigsten Dockerfile Anti-Patterns – von Layer Bloat über Cache-Invalidierung bis zu Secrets im Image – und zeigen jeweils den direkten Fix. Jeder Abschnitt schließt mit einer konkreten Regel, die sich in eine Team-Checkliste für Dockerfile-Reviews übernehmen lässt.

2. Layer Bloat: zu viele RUN-Befehle und unnötige Daten

Das häufigste Dockerfile Anti-Pattern ist die Zerstreuung von Paketinstallationen über mehrere RUN-Befehle. Jeder RUN-Befehl erzeugt einen neuen Layer im Image. Wenn ein Layer Pakete installiert und ein späterer Layer den Cache dieser Pakete löscht, verringert das zwar den Platz im obersten Layer, aber nicht die tatsächliche Image-Größe – denn die gelöschten Dateien existieren weiterhin im vorherigen Layer. Dieses Muster findet sich in vielen offiziellen und inoffiziellen Images und ist für Image-Größen von über einem Gigabyte mitverantwortlich.

Das korrekte Gegenmuster kombiniert alle verwandten Befehle in einem einzigen RUN-Befehl und löscht den Paket-Cache in derselben Anweisung. apt-get install gefolgt von && rm -rf /var/lib/apt/lists/* im gleichen RUN-Block reduziert die Layer-Größe direkt. Gleiches gilt für apk add --no-cache bei Alpine-basierten Images. Ein weiteres Dockerfile Anti-Pattern im Bereich Layer Bloat ist das Kopieren des gesamten Source-Codes vor dem Installationsschritt – das invalidiert den Cache bei jeder Code-Änderung, obwohl sich die Abhängigkeiten nicht geändert haben.


# ANTI-PATTERN: separate RUN layers — cache of deleted files stays in layer 1
FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y curl wget git
RUN rm -rf /var/lib/apt/lists/*   # too late — previous layer retains the cache

# CORRECT: combine into a single RUN, delete cache in the same layer
FROM ubuntu:22.04
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
       curl \
       wget \
       git \
    && rm -rf /var/lib/apt/lists/*

# ANTI-PATTERN: copy all sources before installing dependencies
COPY . /app
RUN pip install -r /app/requirements.txt   # cache busted on every code change

# CORRECT: copy only the dependency manifest first
COPY requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r /app/requirements.txt
COPY . /app   # source copy here — does NOT bust dependency cache

3. Cache-Invalidierung durch falsche Reihenfolge

Der Docker Layer Cache ist eines der mächtigsten Werkzeuge für schnelle Builds – und wird durch die Reihenfolge der Anweisungen im Dockerfile gesteuert. Das zentrale Anti-Pattern: häufig ändernde Daten früh in das Dockerfile zu platzieren. Sobald eine Anweisung den Cache invalidiert, werden alle nachfolgenden Layer neu gebaut – auch wenn sie sich nicht geändert haben. Ein COPY . . am Anfang des Dockerfiles bedeutet, dass jede Code-Änderung alle nachfolgenden Layer – inklusive der oft minutenlangen Abhängigkeitsinstallation – neu auslöst.

Die Lösung ist das Prinzip "vom Seltensten zum Häufigsten": Systempakete, dann Laufzeit-Konfiguration, dann Abhängigkeitsdateien (package.json, composer.json, requirements.txt), dann Abhängigkeiten installieren, zuletzt den eigentlichen Source-Code kopieren. In der Praxis reduziert diese Umsortierung die durchschnittliche Build-Zeit bei Code-Änderungen von Minuten auf Sekunden, weil alle teuren Schritte gecacht bleiben. Das Dockerfile Anti-Pattern der falschen Reihenfolge ist rein logischer Natur – der Build ist korrekt, aber unnötig langsam.


# ANTI-PATTERN: wrong order invalidates cache on every code change
FROM node:22-alpine
WORKDIR /app
COPY . .                    # any file change busts ALL subsequent layers
RUN npm ci                  # expensive — runs on every commit, even doc changes
RUN npm run build

# CORRECT: dependency manifest first, source last
FROM node:22-alpine
WORKDIR /app
# Step 1: only copy manifests (changes rarely)
COPY package.json package-lock.json ./
# Step 2: install dependencies (cached unless manifests change)
RUN npm ci --omit=dev
# Step 3: copy source (changes often, but only triggers fast steps below)
COPY . .
RUN npm run build

# CORRECT PHP example — same principle with Composer
FROM php:8.4-fpm-alpine
WORKDIR /var/www/html
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --optimize-autoloader
COPY . .
RUN composer run-script post-install-cmd

4. Root-User: das häufigste Sicherheits-Anti-Pattern

Container, die als Root laufen, sind eines der am häufigsten diskutierten und gleichzeitig am häufigsten ignorierten Dockerfile Anti-Patterns. Wenn ein Prozess im Container als Root läuft und ein Angreifer eine Schwachstelle in der Anwendung ausnutzt, erhält er Root-Rechte innerhalb des Containers – und bei schwacher Kernel-Isolation potenziell auch auf dem Host. Kubernetes-Cluster mit aktivierter Pod Security Admission lehnen Container mit Root-User in Restricted-Profilen standardmäßig ab. Viele Cloud-Provider und Compliance-Frameworks fordern Non-Root-Container explizit.

Das Fix-Muster ist einfach: einen dedizierten Benutzer im Dockerfile anlegen und vor dem letzten ENTRYPOINT oder CMD mit USER darauf wechseln. Bei Alpine-basierten Images: addgroup -S appgroup && adduser -S appuser -G appgroup. Bei Debian/Ubuntu: groupadd -r appgroup && useradd -r -g appgroup appuser. Kritisch ist dabei, dass die Anwendungsverzeichnisse korrekte Eigentümer bekommen, bevor auf den Non-Root-User gewechselt wird. Ein weiteres subtiles Anti-Pattern: der User wird zwar angelegt, aber Volumes und gemountete Verzeichnisse gehören weiterhin Root, was zur Laufzeit zu Berechtigungsfehlern führt.

5. Unkontrollierter Build-Context ohne .dockerignore

Beim Ausführen von docker build sendet der Docker-Client den gesamten Build-Context an den Docker-Daemon – standardmäßig das aktuelle Verzeichnis und alle darin enthaltenen Dateien. Ohne eine .dockerignore-Datei bedeutet das: Node-Module mit hunderten Megabyte, Git-Repository-Geschichte, lokale Secrets-Dateien, IDE-Konfigurationen und Testdaten landen vollständig im Build-Context und verlangsamen den Build erheblich. Das ist ein klassisches Dockerfile Anti-Pattern, das allein durch eine korrekte .dockerignore-Datei behoben wird.

Die .dockerignore-Datei folgt denselben Musterregeln wie .gitignore. Mindestinhalt für die meisten Projekte: node_modules, .git, *.log, .env*, dist, vendor (PHP/Composer), __pycache__. Bei Projekten mit mehreren Gigabyte an Abhängigkeiten kann das Hinzufügen einer .dockerignore die Zeit für das Senden des Build-Contexts von dreißig Sekunden auf unter eine Sekunde reduzieren. Ein verwandtes Dockerfile Anti-Pattern: der Einsatz von COPY . . ohne vorherige .dockerignore, der dazu führt, dass Secrets und Credentials in das finale Image gelangen.


# .dockerignore — always place in project root alongside Dockerfile
# Prevents secrets, large deps, and VCS history from entering build context

# Version control
.git
.gitignore

# Environment and secrets — critical security item
.env
.env.*
*.pem
*.key
secrets/

# Dependency directories (reinstalled during build)
node_modules/
vendor/
__pycache__/
*.pyc

# Build artifacts and caches
dist/
build/
.cache/
var/cache/
pub/static/

# IDE and OS
.idea/
.vscode/
*.DS_Store
Thumbs.db

# Test and documentation files
tests/
*.test.js
*.spec.ts
README.md
docs/

# Logs
*.log
logs/

6. Fehlende Multi-Stage-Builds bei kompilierten Sprachen

Für Go, Rust, Java, TypeScript und PHP (mit Composer) ist der Multi-Stage-Build kein optionales Feature, sondern eine Grundvoraussetzung für produktionsreife Images. Das Dockerfile Anti-Pattern ohne Multi-Stage-Build: der vollständige Build-Stack – Compiler, SDK, Build-Tools, Dev-Abhängigkeiten – landet im finalen Image, obwohl zur Laufzeit nur das kompilierte Artefakt benötigt wird. Ein Go-Binary, das in einem Image mit dem vollständigen Go-Compiler läuft, ist im besten Fall unnötig groß und im schlechtesten Fall ein Sicherheitsrisiko, weil der Compiler selbst als Angriffswerkzeug genutzt werden kann.

Multi-Stage-Builds lösen dieses Dockerfile Anti-Pattern strukturell: eine builder-Stage enthält alle Build-Tools und kompiliert das Artefakt, eine finale runtime-Stage basiert auf einem minimalen Basis-Image und kopiert nur das fertige Artefakt. Das Ergebnis ist ein Image, das nur enthält, was zur Laufzeit tatsächlich benötigt wird. Für Go-Applikationen kann das Image dabei von 800 MB auf unter 20 MB schrumpfen – nur durch den korrekten Einsatz von Multi-Stage-Builds ohne Änderung am Anwendungscode.

7. Secrets und Credentials im Image – ein fatales Anti-Pattern

Credentials, API-Schlüssel oder SSH-Keys, die über COPY oder ENV in ein Dockerfile gelangen, bleiben dauerhaft in der Image-History eingefroren – auch wenn sie in einem späteren Layer scheinbar gelöscht werden. docker history image-name und docker save machen alle Layer und ihre Inhalte sichtbar. Das ist ein kritisches Dockerfile Anti-Pattern, das in öffentlichen Registries zu unmittelbaren Credential-Lecks führt und in privaten Registries die Angriffsfläche bei einer Kompromittierung erhöht.

Secrets gehören nicht in Dockerfiles – niemals. Die korrekten Alternativen: Build-Time-Secrets über RUN --mount=type=secret (Docker BuildKit), der das Secret nur während des Build-Befehls als temporäres Dateisystem einhängt und es nicht im finalen Image speichert. Zur Laufzeit: Secrets über Umgebungsvariablen aus einer gesicherten Quelle (Vault, Kubernetes Secrets, AWS Secrets Manager) injizieren. Docker Compose ermöglicht das mit dem secrets-Schlüssel. Das Dockerfile Anti-Pattern der eingebackenen Secrets ist unter den zehn häufigsten Sicherheitsproblemen in Container-Umgebungen – und eins der am einfachsten vermeidbaren.


# ANTI-PATTERN: secret baked into image layer — visible in docker history
FROM php:8.4-fpm-alpine
ENV DATABASE_PASSWORD=supersecret123   # permanent in image history
RUN composer install --no-dev
# Even if you later do: RUN unset DATABASE_PASSWORD
# the ENV layer still contains the value in docker history

# CORRECT: BuildKit secret mount — not stored in any layer
# syntax=docker/dockerfile:1
FROM php:8.4-fpm-alpine
COPY composer.json composer.lock ./
# Secret is mounted as /run/secrets/composer_auth only during this RUN
RUN --mount=type=secret,id=composer_auth,dst=/root/.composer/auth.json \
    composer install --no-dev --no-scripts --optimize-autoloader

# CORRECT: runtime injection via Docker Compose secrets
# docker-compose.yml excerpt:
# services:
#   app:
#     secrets:
#       - db_password
# secrets:
#   db_password:
#     file: ./secrets/db_password.txt
# Inside container: /run/secrets/db_password — never in image

# Verify no secrets in image history
docker history --no-trunc your-image:tag | grep -i "password\|secret\|key\|token"

8. Fehlende Healthchecks und falsche ENTRYPOINT-Konfiguration

Container ohne HEALTHCHECK-Direktive werden von Orchestratoren wie Kubernetes und Docker Swarm als "running" behandelt, sobald der Prozess gestartet ist – unabhängig davon, ob die Anwendung tatsächlich bereit ist, Anfragen zu verarbeiten. Dieses Dockerfile Anti-Pattern führt zu Race-Conditions beim Start: ein Load Balancer leitet Anfragen an einen Container weiter, der noch nicht bereit ist, und Nutzer sehen Fehler. Ein korrekt konfigurierter HEALTHCHECK definiert einen Befehl, der den tatsächlichen Zustand der Anwendung prüft, und gibt dem Orchestrator die Information, den Container erst dann als verfügbar zu melden, wenn er wirklich bereit ist.

Ein weiteres Dockerfile Anti-Pattern in diesem Bereich: die Verwendung von Shell-Form für ENTRYPOINT statt der Exec-Form. ENTRYPOINT ["sh", "-c", "myapp"] oder ENTRYPOINT myapp wrappen den Prozess in eine Shell, die POSIX-Signale wie SIGTERM nicht korrekt weiterleitet. Wenn Docker oder Kubernetes einen Container stoppen, erhält die Shell das Signal – aber die eigentliche Anwendung nicht, was zu Timeouts und erzwungenen SIGKILL-Abbrüchen führt. Die Exec-Form ENTRYPOINT ["/app/myapp"] macht den Prozess zum direkten PID 1 und empfängt Signale korrekt.

9. Anti-Patterns im direkten Vergleich

Die häufigsten Dockerfile Anti-Patterns lassen sich kategorisieren: manche betreffen die Performance (Build-Zeit, Image-Größe), andere die Sicherheit (Root-User, Secrets), und einige beides gleichzeitig. Die folgende Tabelle gibt einen strukturierten Überblick der häufigsten Anti-Patterns, ihrer Auswirkung und des direkten Fixes.

Anti-Pattern Kategorie Auswirkung Fix
Separate RUN-Layer Performance Große Images, Bloat bleibt in Layern RUN-Befehle kombinieren, Cache löschen
COPY . . zu früh Performance Cache-Invalidierung bei jeder Änderung Abhängigkeiten vor Source-Code kopieren
Root-User Sicherheit Privilegienausweitung bei Exploits USER-Anweisung mit Non-Root-Account
Kein .dockerignore Performance + Sicherheit Secrets im Image, langsamer Build .dockerignore mit node_modules, .env, .git
Secrets in ENV/COPY Sicherheit Dauerhaft in Image-History eingefroren BuildKit --mount=type=secret
Kein HEALTHCHECK Verfügbarkeit Frühzeitig als verfügbar gemeldete Container HEALTHCHECK mit curl oder wget

Ein vollständiger Dockerfile Review adressiert alle Kategorien gleichzeitig. In der Praxis empfiehlt sich das Tool hadolint (Dockerfile Linter), das viele dieser Dockerfile Anti-Patterns statisch erkennt und per CI-Integration automatisch bei jedem Commit prüft. hadolint Dockerfile gibt nummerierte Warnungen mit direkten Hinweisen zurück. Für Security-Scans von fertigen Images ergänzen Tools wie trivy oder grype den Review um Schwachstellendatenbanken.

Mironsoft

Dockerfile-Optimierung, Container-Security und DevOps-Beratung

Dockerfile Anti-Patterns in eurem Projekt eliminieren?

Wir analysieren bestehende Dockerfiles und Container-Setups, identifizieren kritische Anti-Patterns und setzen Multi-Stage-Builds, korrekte Layer-Reihenfolge und sichere Secret-Verwaltung um.

Dockerfile Review

Analyse mit hadolint, trivy und manueller Anti-Pattern-Prüfung

Image-Optimierung

Multi-Stage-Builds, Layer-Konsolidierung und Build-Context-Kontrolle

CI-Integration

hadolint und trivy in GitLab CI / GitHub Actions einbinden

10. Zusammenfassung und Review-Checkliste

Dockerfile Anti-Patterns entstehen selten durch Nachlässigkeit, sondern durch fehlendes Wissen über die Konsequenzen bestimmter Muster. Die wichtigsten Punkte im Überblick: RUN-Befehle zusammenfassen und Cache im gleichen Layer löschen. Abhängigkeits-Manifeste vor dem Source-Code kopieren. Konsequent Non-Root-User einsetzen. Eine vollständige .dockerignore-Datei pflegen. Multi-Stage-Builds für alle kompilierten Sprachen und Build-Artefakte. Secrets ausschließlich über BuildKit-Secrets oder Runtime-Injection verwalten. HEALTHCHECK und Exec-Form für ENTRYPOINT verwenden.

Die Integration von hadolint als Linting-Schritt in die CI-Pipeline stellt sicher, dass neue Dockerfile Anti-Patterns automatisch erkannt werden, bevor ein Image gebaut und deployed wird. Das ergänzt den manuellen Review und schafft eine Baseline, die für das gesamte Team gilt. Ein einmaliger Review-Sprint, der alle bestehenden Dockerfiles durch diese Checkliste führt, reduziert Image-Größen, verkürzt Build-Zeiten und schließt die häufigsten Sicherheitslücken.

Dockerfile Anti-Patterns — Review-Checkliste auf einen Blick

Performance

RUN-Befehle kombinieren, Cache im selben Layer löschen. COPY-Reihenfolge: Manifeste vor Source. Multi-Stage-Build für Compiler und Build-Tools.

Sicherheit

Non-Root-USER. Keine Secrets in ENV oder COPY. BuildKit --mount=type=secret. .dockerignore mit .env, .git, node_modules.

Verfügbarkeit

HEALTHCHECK mit realistischen start-period und interval-Werten. ENTRYPOINT in Exec-Form für korrekte Signalweiterleitung.

Automatisierung

hadolint in CI-Pipeline integrieren. trivy für Image-Schwachstellenscans. docker history --no-trunc auf Secrets prüfen.

11. FAQ: Dockerfile Anti-Patterns

1Was ist ein Dockerfile Anti-Pattern?
Ein Muster, das funktioniert, aber zu großen Images, langen Build-Zeiten oder Sicherheitsproblemen führt. Separate RUN-Layer, Root-User und COPY . . zu früh sind die häufigsten Beispiele.
2Warum bleibt gelöschter Cache im Image?
Layer sind unveränderlich. Ein in Layer 2 installiertes Paket bleibt in Layer 2, auch wenn Layer 3 es löscht. Nur innerhalb desselben RUN-Befehls sind Installation und Löschung im gleichen Layer.
3Wann lohnt Multi-Stage-Build?
Immer wenn Compiler, Build-Tools oder Dev-Dependencies zur Laufzeit nicht gebraucht werden. Go, Rust, Java, TypeScript, PHP/Composer – praktisch jede kompilierte Sprache profitiert davon.
4Sind Secrets in ENV wirklich ein Problem?
Ja, kritisch. docker history --no-trunc zeigt alle ENV-Werte dauerhaft. Auch späteres Überschreiben löscht den ursprünglichen Layer nicht. BuildKit --mount=type=secret ist die sichere Alternative.
5Shell-Form vs. Exec-Form bei ENTRYPOINT?
Shell-Form startet eine Shell als PID 1, die SIGTERM nicht weiterleitet. Exec-Form macht die Anwendung direkt zu PID 1 und ermöglicht Graceful Shutdown. Immer Exec-Form verwenden.
6Wie prüfe ich Image auf Secrets?
docker history --no-trunc image-name, docker save | tar -tv, und trivy image. Alle drei Methoden decken verschiedene Aspekte ab. trivy erkennt auch bekannte Credential-Patterns.
7Was gehört in .dockerignore?
.git, node_modules, vendor, .env*, *.pem, *.key, dist, build, logs. Minimal – aber diese Einträge verhindern die häufigsten Performance- und Sicherheitsprobleme.
8Wie funktioniert BuildKit Secret Mount?
Das Secret wird als temporäres Dateisystem unter /run/secrets/name eingehängt – nur während des RUN-Befehls. Es wird nicht gecacht und erscheint nicht in docker history. Aufruf: docker build --secret id=name,src=./datei .
9Welche Tools für automatische Dockerfile-Reviews?
hadolint für Dockerfile-Linting, trivy für Image-Schwachstellen, dockle für Security-Best-Practices. Alle drei lassen sich in CI integrieren und ergänzen sich gegenseitig.
10Wie setze ich HEALTHCHECK korrekt auf?
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 CMD curl -f http://localhost:PORT/health || exit 1. start-period gibt der App Zeit zum Starten, bevor Fehlversuche zählen.