in Docker-Containern zusammenbringen
PHP-Projekte brauchen Composer für Backend-Dependencies und Node für Asset-Compilation. Beide in einem Container zu zwingen ist falsch – beide auf dem Host laufen zu lassen ist unpraktisch. Multi-Stage-Builds und sauber getrennte Build-Container lösen das elegant.
Inhaltsverzeichnis
- 1. Das Grundproblem: Node und PHP in einem Projekt
- 2. Klare Rollentrennung: Build-Container vs. Runtime-Container
- 3. Composer in einem eigenen Container betreiben
- 4. Node-Assets in einem separaten Build-Stage kompilieren
- 5. Multi-Stage-Dockerfile für PHP-Node-Projekte
- 6. Build-Reihenfolge in Docker Compose steuern
- 7. Layer-Caching für schnelle Build-Zeiten
- 8. Hybride Builds in CI/CD-Pipelines
- 9. Build-Strategien im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Das Grundproblem: Node und PHP in einem Projekt
Moderne PHP-Projekte wie Magento mit Hyvä, Laravel mit Vite oder Symfony mit Webpack Encore sind hybride Projekte: Sie brauchen PHP und Composer für die serverseitige Logik und Node und Composer in Docker-Containern für die Frontend-Asset-Pipeline. Die naheliegende Lösung – alle Tools in ein einziges Docker-Image zu packen – führt zu riesigen Images, langen Build-Zeiten und Schwierigkeiten, Version-Updates für PHP und Node unabhängig voneinander durchzuführen.
Die andere naheliegende Lösung – PHP im Container, Node auf dem Host – bringt das gegenteilige Problem: Entwickler brauchen die richtige Node-Version auf ihrem lokalen System, Build-Ergebnisse können je nach Host-Node-Version leicht variieren, und CI-Server müssen sowohl Node als auch Docker haben. Der richtige Weg für Node und Composer in Docker-Projekten ist eine saubere Rollentrennung: Node in einem dedizierten Build-Container, Composer ebenfalls als Container, PHP-FPM als Runtime-Container – alle drei koordiniert durch Docker Compose und Multi-Stage-Dockerfiles.
2. Klare Rollentrennung: Build-Container vs. Runtime-Container
Die wichtigste konzeptuelle Trennung bei Node und Composer in Docker-Projekten ist die zwischen Build-Containern und Runtime-Containern. Build-Container führen Schritte aus, die Artefakte erzeugen – Composer-Installation erzeugt den vendor-Ordner, Node-Build erzeugt kompilierte CSS- und JS-Dateien. Runtime-Container führen diese Artefakte aus, brauchen aber die Build-Tools selbst nicht. Ein PHP-FPM-Container braucht weder Composer noch Node – nur die bereits installierten PHP-Packages und die fertigen Assets.
Diese Rollentrennung hat direkte Auswirkungen auf Image-Größe und Sicherheit bei Node und Composer in Docker-Setups. Ein Runtime-Image ohne Composer-Binary und ohne Node-Installation hat eine geringere Angriffsfläche. Wenn eine Sicherheitslücke in Node entdeckt wird, betrifft sie nur den Build-Container, nicht die laufende Applikation. Und der Runtime-Container bleibt schlank: keine npm-Dependencies, kein Node-Interpreter, keine Compiler-Tools – nur PHP und der fertige Code.
# Multi-stage Dockerfile for PHP + Node + Composer project
# Stage 1: Composer install — PHP with Composer only
FROM composer:2.8 AS composer-install
WORKDIR /app
# Copy dependency manifest first — maximize cache reuse
COPY src/composer.json src/composer.lock ./
RUN composer install \
--no-dev \
--no-scripts \
--no-plugins \
--prefer-dist \
--optimize-autoloader \
--no-interaction
# Stage 2: Node build — compile frontend assets
FROM node:20-alpine AS node-build
WORKDIR /app
# Copy package manifest first for better caching
COPY src/app/design/frontend/Mironsoft/default/web/tailwind/package.json \
src/app/design/frontend/Mironsoft/default/web/tailwind/package-lock.json \
./web/tailwind/
RUN cd web/tailwind && npm ci --prefer-offline
# Copy templates needed for Tailwind purge/scan
COPY src/app/design/frontend/Mironsoft/default/ ./frontend/
RUN cd web/tailwind && npm run build
# Stage 3: Runtime — PHP-FPM without build tools
FROM php:8.4-fpm-alpine AS runtime
WORKDIR /var/www/html
# Copy only the artifacts — no Composer, no Node
COPY --from=composer-install /app/vendor ./vendor
COPY --from=node-build /app/frontend/web/css ./web/css
COPY src/ .
3. Composer in einem eigenen Container betreiben
In der lokalen Entwicklung mit Docker Compose läuft Composer idealerweise als kurzlebiger Service, der einmalig ausgeführt wird und dann beendet ist. Das Mark-Shust-Setup bietet das Wrapper-Skript bin/composer, das einen temporären Container mit dem aktuellen Composer-Image startet, das Projektverzeichnis einmountet und den Befehl ausführt. Das Ergebnis – der vendor-Ordner – bleibt auf dem Host und wird im PHP-FPM-Container als Volume gemountet.
Für Node und Composer in Docker-Projekte ist die Composer-Version im Container entscheidend für reproduzierbare Builds. Statt composer:latest sollte immer eine gepinnte Version wie composer:2.8 verwendet werden. Das verhindert, dass ein Composer-Update ohne explizite Entscheidung in den Build-Prozess einfließt. In CI-Pipelines wird der Composer-Container als eigener Job-Step ausgeführt, und der erzeugte vendor-Ordner wird als Artefakt an den nächsten Schritt übergeben.
4. Node-Assets in einem separaten Build-Stage kompilieren
Der Node-Build-Container für Node und Composer in Docker-Projekte hat eine klar definierte Aufgabe: Er nimmt Template-Dateien und Konfiguration entgegen, führt den Asset-Build aus (Tailwind CSS kompilieren, JavaScript bundeln) und produziert fertige Dateien, die der Runtime-Container ausliefert. Alles andere – der node_modules-Ordner, die Build-Tools, die Tailwind-Konfiguration – bleibt im Build-Stage und landet nicht im finalen Image.
Für Hyvä-Projekte mit Tailwind CSS v4 ist der Build-Schritt kritisch: Tailwind scannt alle Template-Dateien, um unbenutzte Utility-Klassen zu entfernen. Der Node-Container braucht daher Zugriff auf alle Phtml- und HTML-Dateien im Theme-Verzeichnis. In einem Node und Composer in Docker-Multi-Stage-Build werden diese Template-Dateien in die Build-Stage kopiert, Tailwind scannt sie und das erzeugte CSS wird in den Runtime-Stage kopiert – ohne dass die Templates selbst im CSS-Build-Artefakt landen.
5. Multi-Stage-Dockerfile für PHP-Node-Projekte
Das Multi-Stage-Dockerfile ist das zentrale Werkzeug, um Node und Composer in Docker in einem einzigen Build-Prozess zu vereinen. Jeder Stage hat seine eigene Basis-Image und seinen eigenen Kontext, aber Artefakte können per COPY --from=stage-name von einem Stage in den nächsten übertragen werden. Das Endresultat ist ein schlankes Runtime-Image, das nur die Outputs der Build-Stages enthält, aber nicht die Build-Tools selbst.
Für komplexere Node und Composer in Docker-Setups lohnt es sich, einen gemeinsamen Basis-Stage zu definieren, der System-Dependencies wie SSL-Zertifikate, PHP-Extensions und grundlegende Tools enthält. Composer-Stage und Node-Stage bauen beide auf diesem Basis-Stage auf. Das verhindert, dass dieselben System-Pakete mehrfach installiert werden. BuildKit, das seit Docker 23 standardmäßig aktiv ist, parallelisiert unabhängige Stages automatisch – Composer-Install und Node-Build laufen gleichzeitig, wenn sie nicht voneinander abhängen.
# Development Compose override: run Node and Composer as on-demand services
services:
# On-demand Composer service — run with: docker compose run --rm composer install
composer:
image: composer:2.8
volumes:
- ./src:/app
- composer-cache:/tmp/composer-cache
working_dir: /app
environment:
COMPOSER_CACHE_DIR: /tmp/composer-cache
profiles:
- tools # Only starts when explicitly requested
# On-demand Node service — run with: docker compose run --rm node npm run build
node:
image: node:20-alpine
volumes:
- ./src/app/design/frontend/Mironsoft/default:/app
- node-modules:/app/web/tailwind/node_modules
working_dir: /app/web/tailwind
command: npm run build
profiles:
- tools
# Dev watcher variant — run with: docker compose run --rm node-watch
node-watch:
image: node:20-alpine
volumes:
- ./src/app/design/frontend/Mironsoft/default:/app
- node-modules:/app/web/tailwind/node_modules
working_dir: /app/web/tailwind
command: npm run watch
profiles:
- tools
volumes:
composer-cache:
node-modules: # Named volume: npm install once, reuse across runs
6. Build-Reihenfolge in Docker Compose steuern
Docker Compose bietet mit depends_on eine Möglichkeit, die Startreihenfolge von Services zu definieren. Für Node und Composer in Docker-Build-Workflows ist das jedoch unzureichend, weil depends_on nur wartet, bis ein Container gestartet ist, nicht bis ein Build-Prozess abgeschlossen ist. Die korrekte Lösung sind zwei Ansätze: Entweder werden Build-Services mit profiles versehen und manuell in der richtigen Reihenfolge ausgeführt, oder der Build läuft vollständig im Multi-Stage-Dockerfile, das Docker intern in der richtigen Reihenfolge abarbeitet.
Für die lokale Entwicklung mit Node und Composer in Docker empfiehlt sich ein Wrapper-Skript, das die Build-Schritte in der korrekten Reihenfolge ausführt: Composer-Install, dann Node-Build, dann Static-Content-Deploy. Dieses Skript dient gleichzeitig als Dokumentation der Build-Abhängigkeiten. In CI/CD-Pipelines wird dieselbe Reihenfolge explizit als Job-Steps definiert, wobei jeder Schritt seinen Erfolg als Artefakt an den nächsten übergibt.
7. Layer-Caching für schnelle Build-Zeiten
Der wichtigste Performance-Hebel bei Node und Composer in Docker-Builds ist das Layer-Caching. Docker cached jeden Build-Step als Layer und führt ihn nur dann erneut aus, wenn sich die Inputs dieses Steps geändert haben. Die wichtigste Konsequenz für Package-Installationen: composer.json und composer.lock müssen vor dem Rest des Quellcodes kopiert werden. Wenn nur eine PHP-Datei geändert wurde, bleibt der Composer-Install-Step gecached und wird übersprungen – selbst wenn der Build in einem neuen Container startet.
Dieselbe Strategie gilt für den Node-Build: Zuerst package.json und package-lock.json kopieren, npm ci ausführen, dann Template-Dateien kopieren und den Build starten. Für Node und Composer in Docker-Projekte in CI mit BuildKit lassen sich Layer-Caches zwischen Builds exportieren und importieren – über --cache-from und --cache-to. In GitHub Actions wird der BuildKit-Cache typischerweise in der Registry gespeichert, sodass auch CI-Builds von gecachten Layern profitieren.
8. Hybride Builds in CI/CD-Pipelines
In CI/CD-Pipelines hat man bei Node und Composer in Docker-Projekten zwei grundlegende Optionen: Entweder läuft alles im Docker-Build (Multi-Stage-Dockerfile, docker build erzeugt das fertige Image), oder CI-native Tools führen Composer und Node aus und packen die Ergebnisse in das Docker-Image. Die erste Option ist reproduzierbarer – alles passiert im Container, auf CI und lokal identisch. Die zweite Option ist oft schneller, weil CI-Provider native Caching für vendor und node_modules bieten.
Für Magento-Projekte mit Hyvä kombiniert man typischerweise beide Ansätze: Composer läuft nativ in CI mit gecachtem vendor-Ordner, Node-Build läuft ebenfalls nativ mit gecachtem node_modules, und das Docker-Image wird dann aus dem vorbereiteten Quellcode gebaut, ohne nochmal Composer oder npm ausführen zu müssen. Das Dockerfile definiert in diesem Fall nur den Runtime-Container. Die explizite Trennung von Node und Composer in Docker-Build-Schritten macht sie unabhängig parallelisierbar.
9. Build-Strategien im direkten Vergleich
Es gibt verschiedene Ansätze, Node und Composer in Docker-Builds zu organisieren. Jeder hat Stärken und Schwächen je nach Projektgröße und Team-Setup.
| Strategie | Reproduzierbarkeit | Build-Speed | Empfehlung |
|---|---|---|---|
| Alles in einem Image | Hoch | Langsam, großes Image | Vermeiden |
| Multi-Stage-Dockerfile | Maximal | Mittel mit Caching | Empfohlen für CI/Prod |
| Separate Build-Container in Compose | Hoch | Schnell (Volumes) | Empfohlen für lokale Entwicklung |
| CI-native Tools + Docker für Runtime | Mittel | Sehr schnell (CI-Cache) | Gut für große Teams mit CI |
| Host-Tools (keine Container) | Niedrig | Schnell | Vermeiden – nicht reproduzierbar |
Für Magento-Projekte mit Hyvä und dem Mark-Shust-Setup ist die empfohlene Kombination: Multi-Stage-Dockerfile für den Production-Build und separate Build-Container in Compose für die lokale Entwicklung. Das gibt maximale Reproduzierbarkeit im Deploy-Prozess und maximale Entwicklungsgeschwindigkeit lokal – weil die benannten Volumes für vendor und node_modules nicht bei jeder Code-Änderung neu gebaut werden.
Mironsoft
PHP-Node-Docker-Architektur, CI/CD-Pipelines und hybride Build-Setups
Node, PHP und Composer sauber containerisieren?
Wir entwerfen die Build-Architektur für euer PHP-Node-Projekt: Multi-Stage-Dockerfiles, Layer-Caching-Strategie und CI-Pipelines, die schnell und reproduzierbar sind.
Dockerfile-Design
Multi-Stage-Dockerfiles mit optimiertem Layer-Caching für PHP-Node-Projekte
CI-Pipeline
GitHub Actions oder GitLab CI mit BuildKit-Caching für schnelle Builds
Dev-Environment
Lokale Compose-Setups mit separaten Build-Containern und benannten Volumes
10. Zusammenfassung
Die saubere Trennung von Node und Composer in Docker-Projekten beginnt mit der Frage: Was ist ein Build-Artefakt, und was ist ein Runtime-Bestandteil? Composer-Packages und kompilierte CSS/JS-Dateien sind Artefakte – sie werden erzeugt und in den Runtime-Container kopiert. Build-Tools, node_modules und Composer-Binaries sind keine Runtime-Bestandteile und gehören nicht ins finale Image. Multi-Stage-Dockerfiles implementieren diese Trennung auf Dockerfile-Ebene, getrennte Compose-Services auf Entwicklungsumgebungsebene.
Layer-Caching ist der größte einzelne Performance-Hebel: Package-Manifests vor dem Quellcode kopieren, damit Installs gecached bleiben. Gepinnte Versionen für composer:2.8 und node:20 sichern Reproduzierbarkeit. Benannte Volumes für vendor und node_modules in der lokalen Entwicklung vermeiden unnötige Reinstallationen. Das Ergebnis ist ein Node und Composer in Docker-Setup, das schnell und reproduzierbar ist – lokal und in CI identisch.
Node, PHP und Composer in Docker — Das Wichtigste auf einen Blick
Rollentrennung
Build-Container erzeugen Artefakte. Runtime-Container führen sie aus. PHP-FPM braucht weder Composer noch Node – nur die fertigen Outputs.
Multi-Stage-Build
COPY --from=stage überträgt Artefakte zwischen Stages. BuildKit parallelisiert unabhängige Stages automatisch für kürzere Build-Zeiten.
Layer-Caching
composer.json und package.json immer vor dem Quellcode kopieren. Packages werden nur bei Lock-File-Änderungen neu installiert.
Lokale Entwicklung
Separate Compose-Services mit profiles: tools für On-Demand-Builds. Benannte Volumes für vendor und node_modules vermeiden Reinstallationen.