Docker · Multi-Stage Build · PHP · Node.js
Mehrstufige Dockerfiles für PHP und Node
richtig bauen und optimieren

Ein mehrstufiges Dockerfile trennt Build-Umgebung und Runtime konsequent. Builder-Stages mit Composer, npm und Build-Tools liefern ihr Ergebnis weiter – ohne selbst im finalen Image zu landen. Das Resultat: kleinere Images, weniger Angriffsfläche und reproduzierbare Builds für PHP- und Node-Projekte.

14 Min. Lesezeit Multi-Stage · Builder-Stage · Layer-Cache · Runtime-Stage Docker 24+ · PHP 8.4 · Node 22

1. Das Grundprinzip mehrstufiger Dockerfiles

Ein mehrstufiges Dockerfile besteht aus mehreren FROM-Blöcken, von denen jeder eine eigenständige Build-Stage definiert. Jede Stage hat Zugriff auf ihre eigenen Tools, Dateien und Pakete. Über den COPY --from=stagename-Befehl können Dateien aus einer Stage in eine andere übertragen werden – ohne die Build-Tools der Quell-Stage mitzunehmen. Das finale Image enthält nur das, was explizit in die letzte Stage kopiert wird. Alles andere – Compiler, Build-Dependencies, temporäre Dateien – verbleibt in den Zwischenstufen und wird nicht Teil des ausgelieferten Images.

Der Unterschied zu einstufigen Dockerfiles ist fundamental: Ohne mehrstufige Dockerfiles muss man entweder Build-Tools im Produktions-Image belassen oder umständliche Multi-Step-Skripte schreiben, die Zwischendateien bereinigen. Beide Wege führen zu schwereren, weniger sicheren Images. Das mehrstufige Dockerfile löst das Problem elegant durch Trennung von Verantwortlichkeiten: jede Stage macht genau eine Sache, und das finale Image erbt nur die Ergebnisse, nicht die Mittel.

2. PHP-Builder-Stage: Composer und Extensions

Die PHP-Builder-Stage in einem mehrstufigen Dockerfile hat eine klare Aufgabe: PHP-Dependencies via Composer installieren und PHP-Extensions kompilieren, die für den Build-Prozess nötig sind. Als Basis-Image empfiehlt sich php:8.4-cli – die CLI-Variante ist schlanker als FPM, hat aber alle nötigen Extension-Build-Tools. Der offizielle Composer-Container stellt den Composer-Binary zur Verfügung, der per COPY --from=composer:2 /usr/bin/composer /usr/bin/composer eingebunden wird.

Wichtig in der Builder-Stage ist die Trennung von System-Dependencies und PHP-Code: Zuerst werden alle Extension-Abhängigkeiten (libicu-dev, libzip-dev, etc.) installiert und Extensions kompiliert. Dann folgt ein separater Layer für composer install. Diese Reihenfolge stellt sicher, dass der Composer-Layer nur invalidiert wird, wenn sich composer.lock ändert – nicht bei Änderungen an System-Paketen. In einem mehrstufigen Dockerfile mit langen Build-Zeiten ist diese Caching-Strategie entscheidend.


# Multi-stage Dockerfile: PHP Builder Stage
# Installs PHP extensions and Composer dependencies

# ── Stage 1: php-deps ─────────────────────────────────────────────────────
FROM php:8.4-cli AS php-deps

# Install system libraries required for PHP extensions
RUN apt-get update && apt-get install -y --no-install-recommends \
    libicu-dev \
    libzip-dev \
    libxml2-dev \
    libonig-dev \
    && rm -rf /var/lib/apt/lists/*

# Compile PHP extensions needed for application build
RUN docker-php-ext-install \
    intl \
    zip \
    soap \
    bcmath \
    pdo_mysql \
    opcache

# Bring in Composer binary from official image — no full Composer image needed
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

WORKDIR /app

# Layer: composer dependencies — only invalidated when composer.lock changes
COPY composer.json composer.lock ./
RUN composer install \
    --no-dev \
    --optimize-autoloader \
    --no-scripts \
    --no-interaction \
    --prefer-dist

# Layer: application code — copied after dependencies for optimal caching
COPY src/ ./src/
COPY app/ ./app/
COPY bin/ ./bin/

3. Node-Builder-Stage: npm und Asset-Build

Die Node-Builder-Stage in einem mehrstufigen Dockerfile übernimmt JavaScript- und CSS-Builds. Als Basis eignet sich node:22-alpine – Alpine-basierte Images sind erheblich kleiner als Debian-basierte. Die Stage installiert npm-Dependencies und führt den Build-Befehl aus. Das Ergebnis – kompilierte CSS-Dateien, gebundelte JavaScript-Bundles, optimierte Assets – wird in die PHP-Stage oder direkt in die Runtime-Stage kopiert.

In einem Projekt mit Tailwind CSS v4 sieht die Node-Stage konkret so aus: package.json und package-lock.json werden zuerst kopiert, dann npm ci ausgeführt. Erst danach folgt der COPY der Tailwind-Konfiguration und der Quelldateien, und schließlich npm run build. Damit ist der npm ci-Layer gecacht, solange sich die Lockdatei nicht ändert. In einem mehrstufigen Dockerfile bedeutet das, dass Node-Module nicht bei jedem CSS-Änderungs-Commit neu heruntergeladen werden – ein erheblicher Zeitgewinn in aktiven Projekten.

4. PHP und Node in einem Dockerfile kombinieren

Das Zusammenführen von PHP- und Node-Builder-Stages in einem einzigen mehrstufigen Dockerfile erfordert eine klare Struktur: Die Node-Stage erzeugt die Assets, die PHP-Stage kompiliert den Code, und die Runtime-Stage nimmt das Beste aus beiden. Mit COPY --from=node-builder /app/web/css/styles.min.css und COPY --from=php-deps /app/vendor ./vendor in der Runtime-Stage können beide Ergebnisse zusammengeführt werden. Docker BuildKit führt beide Builder-Stages standardmäßig parallel aus, wenn keine Abhängigkeit zwischen ihnen besteht – das spart Zeit.

Ein häufiges Problem bei kombinierten mehrstufigen Dockerfiles ist das falsche Stage-Targeting. Mit docker build --target runtime . baut Docker nur bis zur angegebenen Stage und überspringt alle nachfolgenden. Das ist nützlich für Debugging und lokale Entwicklungsbuilds. Für CI-Pipelines baut man immer die vollständige letzte Stage. Die Stage-Namen sollten semantisch gewählt sein: php-deps, node-assets, runtime statt generischer Namen wie stage1, stage2. Das erleichtert Debugging und macht das mehrstufige Dockerfile für neue Teammitglieder sofort verständlich.


# Full multi-stage Dockerfile combining PHP and Node builders
# Both builder stages run in parallel with BuildKit

# ── Stage 1: node-assets ─────────────────────────────────────────────────
FROM node:22-alpine AS node-assets

WORKDIR /app

# Cache npm install layer separately from source code
COPY web/tailwind/package.json web/tailwind/package-lock.json ./web/tailwind/
RUN cd web/tailwind && npm ci --prefer-offline

# Copy Tailwind sources and compile
COPY web/tailwind/ ./web/tailwind/
RUN cd web/tailwind && npm run build
# Output: web/css/styles.min.css

# ── Stage 2: php-deps ────────────────────────────────────────────────────
FROM php:8.4-cli AS php-deps

RUN apt-get update && apt-get install -y --no-install-recommends \
    libicu-dev libzip-dev libxml2-dev && rm -rf /var/lib/apt/lists/*
RUN docker-php-ext-install intl zip bcmath pdo_mysql opcache
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader --no-interaction
COPY . .

# ── Stage 3: runtime ─────────────────────────────────────────────────────
FROM php:8.4-fpm AS runtime

# Only runtime extensions — no build tools
RUN docker-php-ext-install pdo_mysql intl opcache zip bcmath

WORKDIR /var/www/html

# Merge results from both builder stages
COPY --from=php-deps /app /var/www/html
COPY --from=node-assets /app/web/css/styles.min.css /var/www/html/web/css/styles.min.css

# Verify: final image contains no npm, no composer binary
RUN php -v && ! command -v npm && ! command -v composer

EXPOSE 9000
CMD ["php-fpm"]

5. Layer-Cache: Warum die Reihenfolge entscheidend ist

In einem mehrstufigen Dockerfile ist die Layer-Reihenfolge innerhalb jeder Stage das wichtigste Performance-Instrument. Jeder RUN-, COPY- und ADD-Befehl erzeugt einen Layer. Docker vergleicht den Inhalt des neuen Layers mit dem gecachten Layer aus dem vorigen Build. Sobald ein Layer sich ändert, werden alle nachfolgenden Layer dieser Stage invalidiert und neu gebaut. Die Grundregel: Was sich selten ändert, kommt zuerst. Was sich häufig ändert, kommt zuletzt.

Konkret bedeutet das in mehrstufigen Dockerfiles für PHP-Projekte: System-Pakete und PHP-Extensions stehen ganz oben – sie ändern sich selten. Composer-Lock und Dependencies folgen – sie ändern sich bei neuen Paketen. Anwendungscode kommt zuletzt – er ändert sich bei jedem Commit. In der Node-Stage gilt dasselbe: package-lock.json vor dem Source-Code. Wer diese Reihenfolge nicht beachtet, invalidiert den Composer- oder npm-Layer bei jedem Commit und verliert die Haupt-Cache-Ersparnis des mehrstufigen Dockerfiles.

6. Eigene Base-Images für konsistente Environments

In Teams mit mehreren Projekten, die alle ähnliche PHP-Basis-Konfiguration teilen, lohnt sich die Erstellung eigener Base-Images. Ein mehrstufiges Dockerfile kann dann auf diesem Base-Image aufbauen, statt jedes Mal Extension-Compilation zu wiederholen. Das Base-Image enthält alle gemeinsamen PHP-Extensions, PHP-Konfiguration, und Betriebssystem-Tools. Projekt-spezifische Extensions werden in der Builder-Stage hinzugefügt.

Der Workflow: Das Base-Image wird in einer eigenen Registry gepflegt und nur bei PHP-Version-Updates oder Extension-Änderungen neu gebaut. Die Projekt-Dockerfiles starten mit FROM registry.mironsoft.de/php-base:8.4 statt mit dem offiziellen PHP-Image. Das hat zwei Vorteile: Build-Zeiten sind kürzer, weil Extension-Compilation ausgelagert ist. Und alle Projekte laufen auf einer konsistenten Basis, was Debugging erheblich vereinfacht. Für mehrstufige Dockerfiles in größeren Teams ist das Base-Image-Pattern eine der wirksamsten Optimierungen.


# Base image Dockerfile — built once, used by all project Dockerfiles
# Stored in internal registry: registry.mironsoft.de/php-base:8.4

FROM php:8.4-fpm AS base

LABEL maintainer="Mironsoft DevOps <devops@mironsoft.de>"
LABEL org.opencontainers.image.description="PHP 8.4 FPM base with common extensions"

# Install OS packages required by PHP extensions
RUN apt-get update && apt-get install -y --no-install-recommends \
    libicu-dev \
    libzip-dev \
    libxml2-dev \
    libonig-dev \
    libpng-dev \
    libjpeg-dev \
    && rm -rf /var/lib/apt/lists/*

# Compile extensions once — all projects inherit this layer from registry
RUN docker-php-ext-configure gd --with-jpeg \
    && docker-php-ext-install \
        intl zip soap bcmath pdo_mysql opcache gd mbstring

# Shared PHP configuration
COPY docker/php.ini /usr/local/etc/php/conf.d/custom.ini

# Create app user for non-root runtime
RUN useradd -u 1000 -m appuser

# Healthcheck for orchestration
HEALTHCHECK --interval=30s --timeout=5s CMD php-fpm -t || exit 1

# Project Dockerfiles then extend: FROM registry.mironsoft.de/php-base:8.4 AS runtime

7. BuildKit: Parallelisierung und Mount-Cache

Docker BuildKit ist der moderne Build-Backend-Daemon, der mehrstufige Dockerfiles erheblich performanter macht. BuildKit analysiert den Dependency-Graph des Dockerfiles und führt unabhängige Stages automatisch parallel aus. Eine PHP-Builder-Stage und eine Node-Builder-Stage ohne gegenseitige Abhängigkeiten laufen gleichzeitig auf separaten Threads. Das spart in einem typischen PHP+Node-Projekt ein bis drei Minuten Build-Zeit.

Das BuildKit Mount-Cache-Feature ist für mehrstufige Dockerfiles mit Package-Managern besonders wertvoll. Statt npm ci die node_modules in einem Layer zu cachen, kann man den npm-Cache (/root/.npm) als persistenten Mount-Cache anlegen: RUN --mount=type=cache,target=/root/.npm npm ci. Dieser Cache überlebt Layer-Invalidierungen und bleibt auch zwischen Builds bestehen, ohne den Layer-Inhalt zu vergrößern. Dasselbe gilt für den Composer-Cache (/root/.composer) und den APT-Cache. Mit diesen Mount-Caches reduziert sich die Download-Zeit für Dependencies bei Cache-Miss erheblich.

8. Image-Größe systematisch reduzieren

Kleinere Images bedeuten schnellere Pull-Zeiten, weniger Netzwerkkosten und eine kleinere Angriffsfläche. In mehrstufigen Dockerfiles sind die wichtigsten Hebel zur Image-Größenreduzierung: Alpine-basierte Basis-Images für Node-Stages, schlanke Debian-Slim-Images für PHP-Runtime, das Entfernen von Build-Dependencies nach der Extension-Kompilierung, und das Nicht-Mitführen von dev-Composer-Dependencies. Mit composer install --no-dev und anschließendem composer dump-autoload --optimize kann der vendor/-Ordner um 20–40% verkleinert werden.

Ein weiterer Ansatz für mehrstufige Dockerfiles: Distroless-Images als Runtime-Basis. Google's Distroless-PHP-Images enthalten nur PHP-FPM und seine direkten Abhängigkeiten – kein Shell, kein Paketmanager, kein überflüssiges Betriebssystem. Das reduziert Image-Größen dramatisch und eliminiert eine ganze Klasse von Shell-Injection-Angriffsvektoren. Für Magento-Projekte ist das nicht immer praktikabel, da Maintenance-Skripte Shells benötigen, aber für reine API-Services ist Distroless die empfehlenswerte Basis.

9. Einstufige vs. mehrstufige Dockerfiles im Vergleich

Der direkte Vergleich macht deutlich, warum mehrstufige Dockerfiles in Produktionsprojekten Standard sein sollten und einstufige Dockerfiles höchstens für einfache Prototypen geeignet sind.

Kriterium Einstufiges Dockerfile Mehrstufiges Dockerfile Empfehlung
Image-Größe 1–3 GB (inkl. Build-Tools) 200–500 MB (nur Runtime) Mehrstufig
Sicherheit Compiler, npm im Prod-Image Kein Build-Tooling im Runtime-Image Mehrstufig
Build-Parallelisierung Nicht möglich Parallele Stages mit BuildKit Mehrstufig
Dockerfile-Komplexität Einfacher, kürzer Mehr Zeilen, mehr Struktur nötig Situationsabhängig
Lokales Debugging Alle Tools verfügbar Einzelne Stages targeten nötig Einstufig für Dev

Für lokale Entwicklung kann ein einstufiges Dockerfile mit allen Tools praktisch sein. Für Staging und Produktion sind mehrstufige Dockerfiles unbedingt zu empfehlen. Eine gute Praxis ist daher die Pflege zweier Dockerfiles: Dockerfile.dev für lokale Entwicklung mit allen Tools, und Dockerfile als mehrstufiges Dockerfile für CI/CD und Produktion.

Mironsoft

Dockerfile-Architektur, Multi-Stage Builds und Container-Optimierung

Schlanke, sichere Docker-Images für euer PHP- oder Node-Projekt?

Wir analysieren bestehende Dockerfiles, entwerfen mehrstufige Dockerfile-Architekturen für PHP und Node, und optimieren Layer-Cache und BuildKit-Integration für kurze Build-Zeiten in CI/CD-Pipelines.

Dockerfile-Review

Bestehende Dockerfiles analysieren, Layer-Cache-Strategie und Größenoptimierung

Multi-Stage-Architektur

Builder-Stages für PHP, Node und andere Tools mit optimaler Parallelisierung

CI/CD-Integration

BuildKit Registry-Cache, automatisierte Tests der Image-Größe und Security-Scans

10. Zusammenfassung

Mehrstufige Dockerfiles sind das zentrale Pattern für saubere, schlanke und sichere Container-Images in PHP- und Node-Projekten. Sie trennen Build-Umgebung und Runtime durch mehrere FROM-Blöcke, wobei jede Stage über COPY --from selektiv Artefakte weitergibt. Die Layer-Reihenfolge – Dependencies vor Code – maximiert die Cache-Nutzung und minimiert Build-Zeiten. Docker BuildKit parallelisiert unabhängige Stages automatisch und ermöglicht über Mount-Caches persistente Package-Manager-Caches über Layer-Invalidierungen hinaus.

Für Teams mit mehreren Projekten lohnen sich eigene Base-Images, die gemeinsame PHP-Extensions und Konfiguration zentralisieren. Das Resultat eines gut konstruierten mehrstufigen Dockerfiles ist ein Runtime-Image ohne Build-Tools, deutlich kleiner als einstufige Äquivalente, vollständig reproducible und in Sekunden deploybar. Die initiale Komplexität ist durch die langfristigen Betriebsvorteile in jedem Produktionsprojekt gerechtfertigt.

Mehrstufige Dockerfiles — Das Wichtigste auf einen Blick

Stage-Trennung

php-deps, node-assets und runtime als separate Stages. COPY --from überträgt nur Artefakte, keine Build-Tools ins finale Image.

Layer-Cache-Strategie

composer.json/lock und package-lock.json vor dem Code-COPY platzieren. Selten ändernde Layer zuerst für maximale Cache-Nutzung.

BuildKit-Features

Parallele Stage-Ausführung und --mount=type=cache für npm/Composer reduzieren Build-Zeiten erheblich ohne Layer-Größe zu erhöhen.

Base-Images

Eigene Base-Images mit vorkompilierten PHP-Extensions für konsistente Environments und kürzere Build-Zeiten in allen Projekten.

11. FAQ: Mehrstufige Dockerfiles für PHP und Node

1Was ist ein mehrstufiges Dockerfile?
Mehrere FROM-Blöcke. Jede Stage hat eigene Tools. COPY --from überträgt selektiv Artefakte. Finales Image enthält nur Runtime-Content, keine Build-Tools.
2Warum sind mehrstufige Images kleiner?
Composer, npm und Compiler landen nicht im Runtime-Image. Nur PHP-FPM, App-Code und Runtime-Extensions. Typisch 200–500 MB statt 1–3 GB.
3Wie funktioniert Layer-Cache in Multi-Stage?
Dependencies vor Code kopieren. Nur bei Änderungen an composer.lock oder package-lock.json wird der Package-Manager-Layer invalidiert, nicht bei jedem Code-Commit.
4Laufen PHP und Node parallel?
Ja, mit DOCKER_BUILDKIT=1. BuildKit führt unabhängige Stages parallel aus – spart 1–3 Minuten bei PHP+Node-Projekten.
5Was bringt --mount=type=cache?
Persistenter Cache-Mount über Build-Grenzen hinweg. npm und Composer laden aus lokalem Cache statt Registry – schneller und ohne Layer-Größen-Impact.
6Fehler in Builder-Stage debuggen?
docker build --target stagename . und dann docker run --rm -it IMAGE sh. Nur die gewünschte Stage wird gebaut, Shell zum Untersuchen verfügbar.
7Alpine oder Debian als Basis?
Alpine für Node. Debian-Slim für PHP – musl-libc-Probleme mit manchen PHP-Extensions. php:8.4-fpm-bookworm als stabile PHP-Runtime-Basis.
8Mehrstufige Dockerfiles mit Docker Compose?
Ja, mit build.target lokal andere Stage als in Prod targetieren. Entwickler nutzen Builder-Stage mit allen Tools, CI nutzt Runtime-Stage.
9Wann lohnen sich eigene Base-Images?
Ab zwei Projekten mit ähnlichen PHP-Extensions. Zentralisiert Konfiguration, verkürzt Build-Zeiten und stellt konsistente Environments sicher.
10Für lokale Entwicklung geeignet?
Ja, aber oft Dockerfile.dev mit allen Tools. Alternativ --target builder für Stage mit mehr Tools lokal, --target runtime für CI/Prod.