Layers, Caching, Rebuilds und Registry-Strategien
Ein Docker Image ist keine Blackbox. Es ist eine geordnete Sequenz von Read-only-Layers, die zusammen das Dateisystem des Containers bilden. Wer versteht, wie Layers aufgebaut werden, warum der Build-Cache invalidiert wird und wie Registries Images effizient distribuieren, schreibt Dockerfiles, die schnell bauen und kleine Images erzeugen.
Inhaltsverzeichnis
- 1. Das Layer-Modell: wie ein Docker Image aufgebaut ist
- 2. Build-Cache: Regeln und Invalidierung
- 3. Dockerfile-Reihenfolge für maximales Caching
- 4. Multi-Stage Builds: Image-Größe reduzieren
- 5. BuildKit: parallele Builds und erweiterte Caching-Features
- 6. Image-Größe: was wirklich hilft
- 7. Registry-Strategien: lokal, CI und Produktion
- 8. Registry-Typen im Vergleich
- 9. Image-Sicherheit: Tags, Digests und Scanning
- 10. Zusammenfassung
- 11. FAQ
1. Das Layer-Modell: wie ein Docker Image aufgebaut ist
Ein Docker Image besteht aus einer Sequenz von Read-only-Layers, die übereinander gestapelt ein Union-Dateisystem bilden. Jede Anweisung im Dockerfile, die das Dateisystem verändert (RUN, COPY, ADD), erzeugt einen neuen Layer. FROM, ENV, ARG und LABEL erzeugen keine eigenen Layer oder sehr kompakte Metadaten-Layer. Wenn ein Container gestartet wird, legt Docker einen beschreibbaren Container-Layer oben auf die Read-only-Image-Layers – dieser Layer wird beim Löschen des Containers entfernt.
Das Layer-Sharing ist der entscheidende Effizienz-Vorteil des Modells. Wenn zehn Docker Images auf demselben Basis-Image aufbauen, liegt dieses Basis-Image nur einmal auf der Festplatte. Beim Pull aus einer Registry werden nur die Layers übertragen, die lokal noch nicht vorhanden sind. Das macht Layer-Sharing zum zentralen Performance-Mechanismus – sowohl für lokale Disk-Nutzung als auch für Registry-Transfer. Deshalb ist die Wahl des richtigen Basis-Images keine kosmetische Entscheidung, sondern hat direkte Auswirkungen auf Build-Zeit, Image-Größe und Transfer-Kosten.
2. Build-Cache: Regeln und Invalidierung
Der Build-Cache ist das mächtigste Performance-Feature beim Bauen von Docker Images. Docker prüft vor jeder Build-Anweisung, ob ein gecachter Layer existiert, der für dieselbe Anweisung und denselben Eingabe-Zustand erzeugt wurde. Wenn ja, wird der gecachte Layer wiederverwendet – die Anweisung läuft nicht erneut. Das spart bei Anweisungen wie RUN apt-get install oder RUN composer install viele Minuten Build-Zeit.
Der Build-Cache wird invalidiert, sobald eine Anweisung sich ändert oder ihre Eingaben sich ändern. Bei COPY-Anweisungen prüft Docker den Inhalt der kopierten Dateien (Checksums) – eine Änderung in irgendeiner kopierten Datei invalidiert den Layer. Alle nachfolgenden Anweisungen werden dann ebenfalls neu ausgeführt. Das ist die wichtigste Regel für das optimale Anordnen von Dockerfile-Anweisungen: selten ändernde Anweisungen kommen zuerst, häufig ändernde Anweisungen zuletzt. Wird diese Reihenfolge ignoriert, bauen Docker Images in CI bei jedem Commit von Null.
# Dockerfile — optimized layer order for maximum cache reuse
# Rarely-changing instructions first, frequently-changing last
FROM php:8.4-fpm-alpine AS base
# System dependencies — changes rarely, cached for weeks
RUN apk add --no-cache \
libpng-dev libjpeg-dev libwebp-dev libzip-dev icu-dev \
&& docker-php-ext-install gd intl pdo_mysql zip bcmath opcache
# Composer installation — changes only on version upgrades
COPY --from=composer:2.8 /usr/bin/composer /usr/bin/composer
# composer.json and composer.lock — changes on dependency updates
# Copy only these files first, not the whole source tree
COPY composer.json composer.lock ./
# Install PHP dependencies — cached until lock file changes
RUN composer install --no-dev --no-scripts --prefer-dist --optimize-autoloader
# Application source — changes on every commit (put last)
COPY . .
# Post-install scripts — run after source is in place
RUN composer run-script post-install-cmd --no-interaction
# Set file permissions
RUN chown -R www-data:www-data /var/www/html
CMD ["php-fpm"]
Ein häufiger Fehler: COPY . . am Anfang des Dockerfiles. Das kopiert alle Quelldateien in einem Schritt und invalidiert den Cache bei jeder Dateiänderung – egal ob eine PHP-Datei oder die composer.lock geändert wurde. Die Lösung ist das Trennen von COPY composer.json composer.lock ./ vor dem RUN composer install und das nachgelagerte COPY . . erst nach der Dependency-Installation. So wird composer install nur neu ausgeführt, wenn sich die Lock-Datei ändert.
3. Dockerfile-Reihenfolge für maximales Caching
Die optimale Anordnung von Anweisungen in einem Dockerfile für Docker Images folgt einer klaren Hierarchie: Base-Image und System-Dependencies zuerst, dann Dependency-Management-Dateien, dann Dependency-Installation, dann Application-Code. Diese Reihenfolge stellt sicher, dass der teuerste Schritt – System-Package-Installation und Dependency-Installation – gecacht bleibt, bis sich tatsächlich eine Dependency ändert. Ein Commit, der nur PHP-Code ändert, nutzt alle vorherigen Layer aus dem Cache und baut nur den letzten COPY-Layer neu.
Ein weiterer wichtiger Optimierungspunkt ist die Zusammenfassung von RUN-Befehlen mit &&. Jede separate RUN-Anweisung erzeugt einen eigenen Layer, der die Zwischenzustände persistiert. Wenn apt-get install und apt-get clean in zwei separaten RUN-Anweisungen stehen, enthält der erste Layer die vollständigen Package-Cache-Dateien – auch wenn apt-get clean sie im zweiten Layer löscht. Gelöschte Dateien verschwinden nicht aus früheren Layers. Alle Befehle, die zusammen einen logischen Schritt bilden und temporäre Dateien erzeugen, müssen in einer einzigen RUN-Anweisung zusammengefasst werden.
4. Multi-Stage Builds: Image-Größe reduzieren
Multi-Stage Builds sind das wichtigste Feature für kleine Docker Images in Produktionsumgebungen. Die Idee: mehrere FROM-Anweisungen im selben Dockerfile definieren verschiedene Build-Phasen. Build-Tools, Compiler, Test-Dependencies und Cache-Daten aus frühen Phasen landen nicht im finalen Image. Das finale Image enthält nur, was zur Laufzeit tatsächlich benötigt wird. Der Unterschied zwischen einem naiven PHP-Image mit Composer, allen Dev-Dependencies und Build-Tools und einem optimierten Multi-Stage-Image kann 500–800 MB betragen.
Für Docker Images mit PHP und Node (etwa für Hyvä mit Tailwind CSS) ist ein dreistufiger Aufbau sinnvoll: eine PHP-Builder-Stage, die Composer-Dependencies installiert, eine Node-Builder-Stage, die den Tailwind-CSS-Build durchführt, und eine finale Production-Stage, die nur die Laufzeit-Dependencies enthält. Aus der PHP-Builder-Stage wird das vendor/-Verzeichnis kopiert, aus der Node-Builder-Stage der kompilierte CSS-Output in pub/static. Das Produktions-Image enthält weder Composer noch Node noch npm-Packages.
# Multi-stage Dockerfile for PHP + Node application
# Three stages: PHP builder, Node builder, production image
# Stage 1: PHP dependency installation
FROM php:8.4-fpm-alpine AS php-builder
COPY --from=composer:2.8 /usr/bin/composer /usr/bin/composer
WORKDIR /app
COPY composer.json composer.lock ./
# Install all dependencies including dev for testing; prune later if needed
RUN composer install --no-dev --no-scripts --prefer-dist \
&& composer dump-autoload --optimize --no-dev
# Stage 2: Node/Tailwind CSS build
FROM node:22-alpine AS node-builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production=false # install all including devDependencies for build
COPY . .
RUN npm run build # Tailwind CSS production build
# Stage 3: Production image — minimal, no build tools
FROM php:8.4-fpm-alpine AS production
RUN apk add --no-cache libpng libjpeg libwebp libzip icu-libs \
&& docker-php-ext-install gd intl pdo_mysql zip bcmath opcache
WORKDIR /var/www/html
# Copy only what is needed at runtime from previous stages
COPY --from=php-builder /app/vendor ./vendor
COPY --from=node-builder /app/pub/static ./pub/static
COPY . .
# Remove development files from final image
RUN rm -rf tests/ .git/ node_modules/ package*.json \
&& chown -R www-data:www-data /var/www/html
USER www-data
CMD ["php-fpm"]
5. BuildKit: parallele Builds und erweiterte Caching-Features
BuildKit ist seit Docker Engine 23 das Standard-Build-Backend und bringt erhebliche Verbesserungen gegenüber dem klassischen Builder. Die wichtigsten Features für Docker Images: parallele Ausführung unabhängiger Build-Stages, externe Cache-Quellen (--cache-from), Mount-Types für Secrets und Build-Caches, und das neue --cache-to-Flag für das Exportieren des Build-Cache in eine Registry. Parallele Stages reduzieren die Build-Zeit von Multi-Stage-Dockerfiles erheblich, wenn die Stages unabhängig voneinander sind – die PHP-Builder-Stage und die Node-Builder-Stage aus dem vorherigen Beispiel können gleichzeitig laufen.
Das BuildKit Cache-Mount-Feature (RUN --mount=type=cache) ermöglicht es, Package-Manager-Caches zwischen Builds persistent zu halten, ohne sie in einen Image-Layer zu persistieren. Ein pip install- oder apt-get-Cache bleibt zwischen Builds erhalten, landet aber nicht im finalen Docker Image. Das beschleunigt inkrementelle Builds erheblich: der Package-Manager muss nur heruntergeladen werden, was sich tatsächlich geändert hat. Für Registry-based Caching in CI-Pipelines ermöglicht --cache-to type=registry,ref=registry.example.com/cache/myapp das Persistieren des Build-Cache zwischen Pipeline-Runs, ohne einen lokalen Docker-Daemon.
6. Image-Größe: was wirklich hilft
Bei der Optimierung von Docker Image Größen gibt es Maßnahmen mit hohem Effekt und solche mit minimalem Effekt. Der größte Hebel ist das Basis-Image: von ubuntu:24.04 (~80 MB komprimiert) auf debian:bookworm-slim (~30 MB) oder alpine:3.19 (~7 MB) zu wechseln. Alpine-basierte Images sind klein, aber PHP-Extensions müssen aus Quellen kompiliert werden, was die Build-Zeit erhöht. Das offizielle PHP-Alpine-Image ist ein guter Kompromiss. Distroless-Images von Google sind noch kleiner, aber komplexer zu debuggen.
Der zweitgrößte Hebel ist Multi-Stage Builds, um Build-Tools aus dem finalen Image zu entfernen. Kleinen Effekt haben: aggressives RUN apt-get clean (nützlich, aber Basis-Image-Wahl hat mehr Wirkung), manuelle Layer-Reduzierung durch &&-Verkettung (wichtig für Korrektheit, aber selten der Hauptfaktor für Größe). Docker Images mit docker image history imagename inspizieren zeigt, welcher Layer wie viel Speicher belegt – das ist der erste Schritt zur gezielten Optimierung.
7. Registry-Strategien: lokal, CI und Produktion
Eine Docker Image Registry ist das zentrale Verteilungssystem für Container-Images in einem Team. Docker Hub ist kostenlos für öffentliche Images, aber limitiert für private Images und Pulls. Für professionelle Teams empfehlen sich selbst gehostete Registries (Harbor, Gitea Container Registry) oder Cloud-Provider-Registries (GitHub Container Registry, GitLab Registry, AWS ECR). Die Wahl hängt von Zugangskontrolle, Scanning-Anforderungen, Transfer-Kosten und dem bereits verwendeten CI/CD-System ab.
Eine effektive Registry-Strategie für Teams mit CI/CD nutzt mehrere Namespaces oder Repositories: ein Namespace für gecachte Basis-Images (lokal gespiegelt, um Docker-Hub-Rate-Limits zu umgehen), ein Namespace für CI-Build-Artifacts (tagged mit Commit-SHA, kurzlebig), und ein Namespace für Release-Images (tagged mit Semantic-Version, langlebig). Docker Images in CI werden mit dem Commit-SHA getaggt, Release-Images erhalten einen Semantic-Version-Tag. Das ermöglicht es, jederzeit auf den genauen Stand eines Commits zurückzugehen und gleichzeitig menschenlesbare Release-Tags zu haben.
8. Registry-Typen im Vergleich
Die Wahl der richtigen Registry für Docker Images hat direkte Auswirkungen auf Build-Zeiten, Kosten und Sicherheit.
| Registry | Kosten | Scanning | CI-Integration |
|---|---|---|---|
| Docker Hub | Kostenlos (Rate-Limits) | Basic (Pro) | Universell |
| GitHub GHCR | Kostenlos (GitHub Actions) | Dependabot Alerts | Nahtlos mit GitHub Actions |
| GitLab Registry | Im GitLab-Plan enthalten | Trivy-Integration | Nahtlos mit GitLab CI |
| AWS ECR | Pay-per-use | ECR Enhanced Scanning | AWS-Ökosystem |
| Harbor (Self-hosted) | Nur Infrastrukturkosten | Trivy, Clair integriert | Jede CI-Plattform |
Für Teams, die GitLab CI nutzen, ist die GitLab Container Registry die naheliegende Wahl: kein separater Login nötig, automatische Credentials über $CI_REGISTRY_*-Variablen, integriertes Scanning und einfaches Cleanup alter Docker Images über GitLab-Policies. Der CI-Job kann direkt auf das Registry pushen, ohne separate Secrets zu konfigurieren. Das reduziert den Setup-Aufwand erheblich.
9. Image-Sicherheit: Tags, Digests und Scanning
Tags sind mutable in Docker-Registries: ein latest-Tag kann heute auf ein anderes Docker Image zeigen als gestern. Das macht Tags für reproduzierbare Deployments ungeeignet. Image-Digests (sha256:abc123...) sind unveränderliche Referenzen auf genau einen Image-Inhalt. In Produktions-Deployments sollte immer der Digest verwendet werden, nicht ein Tag: image: nginx@sha256:abc123 statt image: nginx:1.26. Das stellt sicher, dass ein Deployment immer dasselbe Image startet.
Image-Scanning mit Tools wie Trivy oder Grype prüft Docker Images auf bekannte CVEs in installierten Packages und Bibliotheken. Das Scanning sollte in der CI-Pipeline nach dem Build laufen und bei kritischen CVEs den Build blockieren. Trivy kann als Container in GitLab CI ausgeführt werden und gibt strukturierten Output für die GitLab Security Dashboard Integration. Regelmäßiges Rebuild von Base-Images – auch ohne Code-Änderungen – stellt sicher, dass Sicherheits-Patches aus dem Basis-Image zeitnah in eigene Images übernommen werden.
10. Zusammenfassung
Docker Images effizient zu bauen erfordert Verständnis des Layer-Modells und der Build-Cache-Logik. Die wichtigste Optimierung ist die Reihenfolge der Dockerfile-Anweisungen: selten ändernde Schritte zuerst, häufig ändernde Schritte zuletzt. Multi-Stage Builds reduzieren die finale Image-Größe, indem Build-Tools und temporäre Dateien nicht im Produktions-Image landen. BuildKit parallelisiert unabhängige Build-Stages und ermöglicht Registry-based Cache für CI-Pipelines.
Registry-Strategien mit mehreren Namespaces – Basis-Image-Spiegel, CI-Artifacts und Release-Images – ermöglichen reproduzierbare Deployments und einfaches Rollback. Image-Digests statt Tags in Produktionsumgebungen garantieren Unveränderlichkeit. Regelmäßiges Scanning mit Trivy und automatischer Rebuild von Base-Images halten Docker Images sicher. Wer diese Prinzipien kennt und konsequent anwendet, baut Images, die schnell gebaut werden, klein sind und sicher deployed werden können.
Mironsoft
Dockerfile-Optimierung, CI/CD-Pipelines und Container-Infrastruktur
Langsame Docker-Builds in eurer CI?
Wir optimieren Dockerfiles, konfigurieren Registry-based Caching mit BuildKit und richten Image-Scanning ein – damit eure CI-Builds schnell sind und nur sichere Images deployed werden.
Dockerfile-Audit
Analyse der Layer-Reihenfolge, Cache-Invalidierungen und ungenutzten Build-Steps
Multi-Stage Refactoring
Build-Tools aus Produktions-Images entfernen, Image-Größe halbieren
Registry-Setup
Registry-Strategie, BuildKit-Caching in CI und automatisches Image-Scanning
Docker Images — Das Wichtigste auf einen Blick
Layer-Reihenfolge
Selten ändernde Schritte zuerst. COPY composer.lock + RUN composer install vor COPY . .. Cache-Invalidierung bei jeder Source-Dateiänderung vermeiden.
Multi-Stage Builds
Build-Tools und Dev-Dependencies in früheren Stages halten. Nur Laufzeit-Artifacts ins finale Image kopieren. Größenreduktion von 500–800 MB möglich.
Registry-Strategie
CI-Artifacts mit Commit-SHA taggen. Release-Images mit Semantic Version. Digests statt Tags für reproduzierbare Produktions-Deployments.
Sicherheit
Trivy-Scanning in CI nach jedem Build. Regelmäßiger Base-Image-Rebuild für Security-Patches. Image-Digests in Produktions-Deployments.
11. FAQ: Docker Images, Layers und Registry-Strategien
1Warum ist Dockerfile-Reihenfolge wichtig?
2Was ist ein Multi-Stage Dockerfile?
3Digests statt Tags verwenden?
4Was ist BuildKit?
5Build-Cache in CI nutzen?
--cache-from type=registry + --cache-to type=registry. Cache in Registry persistiert, beim nächsten Run geladen.6apt-get clean im selben RUN?
7Registry für GitLab CI?
8Layer-Größen analysieren?
docker image history imagename zeigt alle Layer mit Größe und erzeugender Anweisung.