Docker · Node · PHP · Composer · Multi-Stage
Node, PHP und Composer
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.

11 Min. Lesezeit Multi-Stage · Composer · Node · Asset-Pipeline · CI PHP 8.4 · Node 20 · Composer 2 · Docker Compose

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.

11. FAQ: Node, PHP und Composer in Docker

1Warum Node und PHP nicht in dasselbe Image?
Großes Image, gekoppelte Updates, Build-Tools im Runtime. Getrennte Stages geben Flexibilität und halten Images schlank.
2Was macht COPY --from in Multi-Stage-Builds?
Kopiert Dateien aus einem vorherigen Build-Stage. So landen vendor und CSS im Runtime-Container ohne die Build-Tools selbst.
3Composer-Packages optimal cachen?
composer.json und composer.lock vor dem Quellcode kopieren. Install-Layer bleibt gecacht wenn nur PHP-Dateien sich ändern.
4Wann --no-scripts bei composer install?
Immer wenn der Quellcode noch unvollständig ist. Post-Install-Scripts erwarten oft vollständige Projektstruktur. Skripte separat nach vollständigem COPY ausführen.
5npm run build in Docker ohne Host?
docker compose run --rm node npm run build. Startet Container, baut, gibt Kontrolle zurück. Ergebnis im gemounteten Volume.
6Wann composer:2.8 als Image verwenden?
Als Build-Stage oder kurzlebiger Compose-Service. Enthält nur Composer und PHP. Für Production-Images eigenen PHP-Image als Basis nehmen.
7npm install nicht bei jeder Änderung?
Benanntes Volume für node_modules. Persistiert zwischen Starts. install nur bei package.json-Änderungen oder manuell ausgelöst.
8Composer und Node auch in CI in Containern?
Nicht zwingend. CI-native Actions mit eigenem Caching sind oft schneller. Docker-Image dann ohne erneutes Install bauen – vendor direkt kopieren.
9BuildKit parallelisiert Composer und Node?
Ja, wenn Stages unabhängig sind. composer-install und node-build laufen parallel – kürzere Gesamtbuild-Zeit bei mehrstufigen Builds.
10Private Composer-Repositories in Docker?
BuildKit Secrets für Auth-Token. Nie Credentials in Dockerfile-Schichten – bleiben im Layer-Cache. Secrets sind nur während Build verfügbar.