Distroless Images für PHP-Anwendungen nutzen
AI generated
FROM
RUN
Docker · PHP · Container Security · Image Building
Distroless Images für PHP-Anwendungen nutzen
minimale Laufzeit, minimale Angriffsfläche

Distroless Images enthalten weder Shell noch Paketmanager noch überflüssige Systembibliotheken und reduzieren dadurch die Angriffsfläche eines PHP-Containers drastisch. Mit Static-PHP-CLI oder FrankenPHP als Basis lässt sich eine komplette PHP-Laufzeit in ein Distroless-Image kompilieren, ohne die klassische PHP-FPM-Distribution zu benötigen.

18 Min. Lesezeit Distroless · Static PHP · FrankenPHP · Multi-Stage Docker · PHP 8.4 · Container-Security

1. Was Distroless Images sind und warum PHP-Teams sie brauchen

Ein Distroless Image enthält nur die Anwendung selbst und ihre direkten Laufzeitabhängigkeiten, aber keine Distribution im klassischen Sinn. Kein apt, kein apk, keine Shell, kein coreutils-Paket. Google pflegt die bekannteste Distroless-Basis-Image-Familie unter gcr.io/distroless, mit Varianten für statische Binaries, für Java, für Node.js und für C-Bibliotheken. Für PHP gibt es keine offizielle Distroless-Variante, weil der klassische PHP-Interpreter dynamisch gegen libphp, libxml2 und ein Dutzend weiterer Shared Libraries linkt.

Der Reiz von Distroless Images liegt in der drastisch reduzierten Angriffsfläche. Ein Angreifer, der über eine Anwendungslücke Codeausführung erreicht, findet in einem Distroless-Container weder bash noch curl noch einen Paketmanager vor, mit dem er sich lateral bewegen oder Werkzeuge nachladen könnte. Vulnerability-Scanner wie Trivy oder Grype melden für ein typisches Distroless-Image oft nur eine Handvoll CVEs, während ein vergleichbares Debian-Slim-Image mit vollständigem Basissystem leicht dreistellig ausfällt. Für PHP-Anwendungen im produktiven Betrieb, insbesondere in regulierten Branchen, ist das ein Argument, das über reine Image-Größe hinausgeht.

Die Kernfrage lautet also: Wie bekommt man eine PHP-Laufzeit in ein Distroless Image, wenn PHP traditionell dynamisch linkt und eine Shell für Entrypoint-Skripte erwartet. Die Antwort liegt in statisch kompiliertem PHP, das keine externen Shared Libraries mehr benötigt, kombiniert mit einem Mehrstufigen Build, der die Kompilierung in einer Build-Stage erledigt und nur die fertige Binary in die Distroless-Runtime-Stage kopiert.

2. Die Herausforderung: PHP braucht eigentlich eine Laufzeit

Ein Standard-PHP-Container basiert auf php:8.4-fpm oder php:8.4-cli, beides Debian-basierte Images mit vollständigem Basissystem. Der PHP-Interpreter selbst ist dynamisch gegen libc, libxml2, libssl, libcurl und weitere Bibliotheken gelinkt, zusätzlich bringen viele Extensions wie pdo_mysql, gd oder intl eigene native Abhängigkeiten mit. Ein Distroless Image ohne diese Bibliotheken würde den Interpreter beim Start sofort mit einem Linker-Fehler abbrechen lassen, weil die .so-Dateien fehlen.

Ein zweites Problem betrifft Entrypoint-Skripte. Viele PHP-Container nutzen ein docker-entrypoint.sh, das vor dem Start Umgebungsvariablen prüft, Konfigurationsdateien generiert oder Wartezeiten auf Datenbanken implementiert. Solche Skripte benötigen zwingend eine Shell, die in einem echten Distroless Image per Definition fehlt. Wer auf Distroless umsteigt, muss diese Logik entweder in die Anwendung selbst verlagern, als kompilierte Init-Binary bereitstellen, oder auf einen Orchestrator wie Kubernetes mit Init-Containern auslagern, die in einem separaten Image mit Shell laufen.

Diese beiden Hürden, dynamisches Linking und Shell-Abhängigkeit, sind der Grund, warum Distroless Images für PHP in der Praxis seltener zu sehen sind als für Go oder Java. Sie sind aber lösbar, wenn man den Build-Prozess konsequent auf statische Kompilierung umstellt und Orchestrierungslogik aus dem Container-Start heraushält.

3. Static-PHP-CLI: eine PHP-Binary ohne externe Abhängigkeiten

Das Projekt static-php-cli kompiliert PHP inklusive der gängigen Extensions zu einer einzigen statischen Binary, die keine externen Shared Libraries mehr benötigt. Alle Abhängigkeiten, von libxml2 bis openssl, werden zur Build-Zeit statisch in die Binary eingebettet. Das Ergebnis ist eine PHP-Binary, die in einem Distroless Image ohne einzige zusätzliche Systembibliothek lauffähig ist, weil sie schlicht keine dynamischen Verweise mehr enthält.

Der Kompilierungsprozess läuft in einem separaten Build-Container mit vollem Toolchain-Zugriff, Compiler, Header-Dateien und Build-Abhängigkeiten für jede Extension. Für Magento-typische Extensions wie gd, intl, bcmath, soap und opcache unterstützt static-php-cli die statische Einbindung inzwischen vollständig, wobei Extensions mit komplexen externen Abhängigkeiten wie imagick mehr Konfigurationsaufwand erfordern.


# Stage 1: compile a fully static PHP binary with static-php-cli
FROM ubuntu:24.04 AS builder

RUN apt-get update && apt-get install -y \
    curl php-cli php-xml php-curl unzip git build-essential \
    autoconf bison re2c pkg-config libsqlite3-dev

WORKDIR /build
RUN curl -sSL https://dl.static-php.dev/static-php-cli/spc-linux-x86_64.tar.gz \
    | tar -xz && mv spc-linux-x86_64 spc

# Download required source packages
RUN ./spc download --for-extensions="bcmath,gd,intl,opcache,pdo_mysql,soap,zip" \
    --with-php=8.4

# Build a static CLI binary with the extensions our app needs
RUN ./spc build "bcmath,gd,intl,opcache,pdo_mysql,soap,zip" \
    --build-cli --build-micro

# Stage 2: distroless runtime with only the static binary
FROM gcr.io/distroless/static-debian12:nonroot AS runtime
COPY --from=builder /build/buildroot/bin/php /usr/local/bin/php
COPY --chown=nonroot:nonroot ./src /app
WORKDIR /app
USER nonroot
ENTRYPOINT ["php", "/app/bin/console"]

Dieser zweistufige Aufbau trennt sauber zwischen Build-Umgebung und Laufzeitumgebung. In der Build-Stage darf beliebig viel installiert werden, weil nur das Endergebnis, die statische Binary, in das Distroless Image übernommen wird. Die Runtime-Stage bleibt minimal, ohne Compiler, ohne Paketmanager, ohne Shell, mit einem nicht-privilegierten Standardnutzer aus der nonroot-Variante.

5. FrankenPHP als Alternative zu PHP-FPM plus Nginx

FrankenPHP verfolgt einen anderen Ansatz für Distroless Images: Es kompiliert PHP als Modul direkt in einen Go-basierten Webserver, sodass keine separate PHP-FPM-Prozessgruppe und kein separater Nginx-Container mehr nötig sind. Der resultierende Server ist eine einzige statisch kompilierbare Go-Binary mit eingebetteter PHP-Laufzeit, was ihn architektonisch näher an klassische Distroless-Kandidaten wie Go-Anwendungen bringt als der klassische PHP-FPM-Stack.

Für ein Distroless Image mit FrankenPHP entfällt zusätzlich die Notwendigkeit, den Unix-Socket zwischen Nginx und PHP-FPM zu konfigurieren, was bei minimalen Basis-Images regelmäßig zu Berechtigungsproblemen führt. FrankenPHP terminiert HTTP direkt, unterstützt HTTP/2 und HTTP/3 nativ und reduziert damit die Anzahl der Prozesse im Container von zwei, FPM und Webserver, auf einen einzigen.


# Stage 1: build FrankenPHP binary statically with the app embedded
FROM dunglas/frankenphp:static-builder AS builder
WORKDIR /go/src/app
COPY ./src /app/public
RUN EMBED=/app ./build-static.sh

# Stage 2: distroless-style runtime, single self-contained binary
FROM gcr.io/distroless/base-debian12:nonroot AS runtime
COPY --from=builder /go/src/app/frankenphp-linux-x86_64 /frankenphp
USER nonroot
EXPOSE 8080
ENTRYPOINT ["/frankenphp", "php-server", "--listen", ":8080"]

4. Mehrstufiger Dockerfile-Aufbau für Distroless-Laufzeiten

Der Schlüssel zu jedem Distroless Image ist die konsequente Trennung von Build- und Runtime-Stage im Dockerfile. Composer-Installation, Asset-Kompilierung, statische Analyse und das Kompilieren der PHP-Binary gehören ausschließlich in Build-Stages mit vollem Werkzeugkasten. Nur Artefakte, die im laufenden Betrieb tatsächlich gebraucht werden, wandern über COPY --from=builder in die finale Stage.

Ein häufiger Fehler beim Umstieg auf Distroless Images ist, vergessene Debug-Werkzeuge oder Composer-Cache-Verzeichnisse versehentlich in die Runtime-Stage zu kopieren, etwa durch ein zu breites COPY . . statt einer gezielten Artefaktliste. Ein .dockerignore mit expliziten Ausschlüssen für vendor/bin, Testverzeichnisse und Entwicklungswerkzeuge verhindert, dass sich unnötige Dateien wieder in das minimale Image einschleichen und den Vorteil von Distroless untergraben.

6. Debugging ohne Shell: Ephemeral Debug Container

Der offensichtlichste Nachteil von Distroless Images ist das Fehlen von sh, bash, ls oder cat im laufenden Container. Ein klassisches docker exec -it container sh schlägt fehl, weil keine Shell existiert, an die exec sich anhängen könnte. Für Kubernetes-Umgebungen löst kubectl debug mit einem Ephemeral Container das Problem elegant: Ein temporärer Debug-Container mit vollem Werkzeugkasten wird in den Namespace des Ziel-Pods eingehängt, ohne den laufenden Distroless-Container selbst zu verändern.

Für reines Docker ohne Kubernetes bietet sich docker debug aus Docker Desktop an, das ähnlich einen temporären Shell-Layer über einen laufenden Container legt, ohne das Image selbst neu bauen zu müssen. Wichtig ist, diese Debug-Werkzeuge niemals dauerhaft im produktiven Distroless Image zu belassen, sondern ausschließlich temporär und bei Bedarf hinzuzufügen.


# Attach an ephemeral debug container to a running distroless pod
kubectl debug -it app-pod-7d9f8 \
  --image=busybox:1.36 \
  --target=php-app \
  -- sh

# Inspect the process namespace shared with the distroless container
ps aux
cat /proc/1/root/app/composer.lock

# Docker Desktop equivalent for a locally running distroless container
docker debug php-distroless-container

7. Health Checks und Prozessmanagement ohne Shell-Tools

Ein klassischer Docker-HEALTHCHECK mit CMD curl -f http://localhost/health || exit 1 funktioniert in einem Distroless Image nicht, weil weder curl noch eine Shell zur Verfügung stehen, die das || auswerten könnte. Die Lösung liegt in einer eigenen, statisch kompilierten Health-Check-Binary, die direkt als Prozess ausgeführt wird und über ihren Exit-Code Erfolg oder Misserfolg signalisiert, ganz ohne Shell-Logik.

Für Magento- oder Symfony-Anwendungen mit FrankenPHP genügt oft ein kleines PHP-Skript, das per php health-check.php direkt als HEALTHCHECK CMD läuft, weil die PHP-Binary selbst als Interpreter fungiert und keine externe Shell benötigt. Prozessmanagement in Kubernetes übernimmt ohnehin der Kubelet über Liveness- und Readiness-Probes, sodass ein Distroless Image hier keine zusätzliche Init-Logik im Container selbst benötigt, solange die Probes korrekt konfiguriert sind.


# Healthcheck without shell: run the static PHP binary directly
HEALTHCHECK --interval=10s --timeout=3s --retries=3 \
  CMD ["/usr/local/bin/php", "/app/bin/health-check.php"]

8. CI-Integration: bauen, scannen, signieren

In der CI-Pipeline lohnt es sich, Distroless Images systematisch mit einem Vulnerability-Scanner wie Trivy zu prüfen, bevor sie in die Registry gepusht werden, um den erwarteten CVE-Rückgang messbar zu dokumentieren. Ein Vergleichslauf gegen das vorherige Debian-Slim-Image zeigt in der Praxis regelmäßig eine Reduktion der gemeldeten CVEs um achtzig bis fünfundneunzig Prozent, weil praktisch das gesamte Betriebssystem-Paket-Ökosystem entfällt.

Zusätzlich empfiehlt sich, das gebaute Distroless Image vor dem Deployment zu signieren, etwa mit Cosign, um sicherzustellen, dass in der Produktion nur Images landen, die tatsächlich aus der eigenen Pipeline stammen. Diese Kombination aus minimaler Angriffsfläche und kryptografisch verifizierter Herkunft ergibt eine deutlich robustere Supply-Chain als der klassische Ansatz mit vollem Basis-Image ohne Signatur.


# GitLab CI: build distroless image, scan, then sign
build-distroless:
  stage: build
  script:
    - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" .
    - trivy image --exit-code 1 --severity CRITICAL,HIGH "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
    - cosign sign --key cosign.key "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"

9. Distroless im Vergleich zu Debian-Slim und Alpine

Die Wahl zwischen Distroless, Debian-Slim und Alpine für PHP-Workloads hängt stark davon ab, wie viel Kontrolle über Debugging-Fähigkeiten im laufenden Betrieb wichtig ist, gegenüber der Priorität einer minimalen Angriffsfläche. Die folgende Tabelle stellt die drei Ansätze für typische PHP-Produktionsanwendungen gegenüber.

Kriterium Debian-Slim Alpine Distroless
Shell im Container Ja, bash Ja, ash Nein
Paketmanager apt vorhanden apk vorhanden Keiner
Typische CVE-Anzahl Mittel bis hoch Niedrig bis mittel Sehr niedrig
Build-Aufwand für PHP Gering Mittel, musl-Kompatibilität Hoch, Static-PHP nötig
Debugging im Live-Container Direkt möglich Direkt möglich Nur via Ephemeral Container

Für Teams, die Kubernetes mit Ephemeral-Debug-Containern bereits produktiv nutzen, überwiegt der Sicherheitsgewinn eines Distroless Image den Mehraufwand beim Debugging deutlich. Für kleinere Teams ohne Kubernetes-Orchestrierung ist ein gut gepflegtes Debian-Slim-Image mit regelmäßigen Sicherheitsupdates oft der pragmatischere Kompromiss zwischen Aufwand und Nutzen.

Mironsoft

Container-Security und Docker-Buildstrategien für PHP-Anwendungen

Distroless Images für eure PHP-Produktion?

Wir bauen mehrstufige Dockerfiles mit Static-PHP oder FrankenPHP, richten CI-Scans mit Trivy und Image-Signierung mit Cosign ein und begleiten den Umstieg auf Distroless-Runtime-Images ohne Betriebsrisiko.

Static-PHP-Builds

Kompilierung eurer PHP-Extensions als statische Binary für Distroless-Laufzeiten

Dockerfile-Umbau

Mehrstufige Builds mit sauberer Trennung von Build- und Runtime-Stage

CI-Sicherheit

Trivy-Scans, Cosign-Signaturen und SBOM-Generierung in der Pipeline verankern

10. Zusammenfassung

Distroless Images für PHP-Anwendungen reduzieren die Angriffsfläche eines Containers auf ein Minimum, weil weder Shell noch Paketmanager noch überflüssige Systembibliotheken vorhanden sind. Der Weg dorthin führt über statisch kompiliertes PHP mit Static-PHP-CLI oder über FrankenPHP als eingebettete Go-Binary, jeweils gebaut in einer separaten Compiler-Stage und nur als fertiges Artefakt in die Distroless-Runtime kopiert. Debugging verlagert sich von direktem Shell-Zugriff auf Ephemeral Debug Container, Health Checks laufen über kompilierte Binaries statt über Shell-Skripte mit curl.

Der Umstieg lohnt sich besonders für regulierte Umgebungen und für Teams, die CVE-Zahlen als harte Metrik in Sicherheitsaudits vorweisen müssen. Wer bereits Kubernetes mit Ephemeral Containern nutzt, verliert beim Umstieg auf Distroless Images praktisch keine Debugging-Fähigkeit, gewinnt aber eine massiv reduzierte Angriffsfläche und eine deutlich kleinere Anzahl gemeldeter Schwachstellen pro Image.

Distroless Images für PHP-Anwendungen — Das Wichtigste auf einen Blick

Static-PHP-CLI

Kompiliert PHP samt Extensions als statische Binary ohne externe Shared Libraries, Voraussetzung für ein echtes Distroless-Image.

FrankenPHP

Ersetzt PHP-FPM plus Nginx durch eine einzige Go-Binary mit eingebetteter PHP-Laufzeit, ideal für minimale Container.

Debugging ohne Shell

Ephemeral Debug Container in Kubernetes oder docker debug statt exec -it container sh.

CVE-Reduktion

Typisch achtzig bis fünfundneunzig Prozent weniger gemeldete CVEs gegenüber Debian-Slim, weil das Basis-Betriebssystem entfällt.

11. FAQ: Distroless Images für PHP-Anwendungen

1Was ist ein Distroless Image?
Nur Anwendung und direkte Laufzeitabhängigkeiten, ohne Shell, Paketmanager oder volles Betriebssystem-Ökosystem.
2Warum ist PHP schwieriger als Go?
PHP linkt dynamisch gegen viele Shared Libraries. Static-PHP-CLI löst das durch vollständig statische Kompilierung.
3Was ist static-php-cli?
Ein Projekt, das PHP samt Extensions zu einer einzigen statischen Binary ohne externe Abhängigkeiten kompiliert.
4Kann Composer noch genutzt werden?
Ja, in der Build-Stage. Nur vendor-Ordner und Binary wandern in die finale Distroless-Stage.
5Wie debugge ich einen laufenden Container?
Mit kubectl debug oder docker debug, jeweils über einen temporären Ephemeral Debug Container.
6Funktioniert curl im HEALTHCHECK?
Nein. Healthcheck läuft als kompilierte Binary oder direkt per PHP-Skript über den Interpreter.
7Was ist FrankenPHP?
Ersetzt PHP-FPM und Nginx durch eine einzige Go-Binary mit eingebetteter PHP-Laufzeit.
8Wie viel kleiner wird das Image?
Größe oft sechzig bis achtzig Prozent kleiner, CVEs typischerweise um achtzig bis fünfundneunzig Prozent reduziert.
9Brauche ich Kubernetes dafür?
Nein, funktioniert auch mit reinem Docker. Docker Desktop bietet mit docker debug eine vergleichbare Funktion.
10Welche Extensions sind am aufwendigsten?
Extensions mit komplexen externen Abhängigkeiten wie imagick, im Gegensatz zu direkt unterstützten wie bcmath, gd, intl oder pdo_mysql.