FrankenPHP im Produktionsbetrieb: Modernes PHP-Deployment ohne PHP-FPM
Wenn du eine Symfony-Anwendung das erste Mal hinter FrankenPHP im Produktionsbetrieb stellst und der Request-Graph in Grafana sich abflacht, fühlt es sich wie Schummeln an. Gleicher Code, gleiche Datenbank, gleicher Redis-Cluster - das Einzige, das sich geändert hat, ist dass du das Nginx + PHP-FPM-Sandwich entfernt und durch eine einzelne Go-Binary ersetzt hast, die HTTP/3 spricht, deine Anwendung zwischen Requests im Speicher hält und deine Assets als statische Dateien vom gleichen Prozess ausliefert. P95-Latenzen sinken, das Container-Image schrumpft und das ganze "Welcher Prozess soll eigentlich wo laufen"-Gespräch hört beim Deployment auf.
Das ist die optimistische Version der FrankenPHP-Produktionsgeschichte. Die realistische Version hat noch ein paar weitere Kapitel: Worker-Mode-Anwendungen verhalten sich anders als Request-per-Process-Anwendungen, langlebige PHP-Worker lecken Speicher auf Arten, die PHP-FPM zwei Jahrzehnte lang verborgen hat, und einige Symfony-Bundles gehen noch immer von einem frischen Kernel-Boot pro Request aus. Keines davon ist ein Dealbreaker, aber das ist der Unterschied zwischen einer erfolgreichen Migration und einem Rollback um 2 Uhr morgens. Dieser Beitrag zeigt, was FrankenPHP architektonisch tatsächlich ändert, wann sich der Worker-Mode für Symfony-Anwendungen lohnt, den Migrationspfad vom traditionellen Stack und die operativen Besonderheiten, die es zu verstehen gilt.
Was FrankenPHP tatsächlich ändert
FrankenPHP ist ein in Go geschriebener Server, der den PHP-Interpreter direkt einbettet. Er basiert auf Caddy - demselben Webserver, der automatisches HTTPS und HTTP/3 liefert - und kann als Drop-in-Ersatz für die Nginx + PHP-FPM-Kombination fungieren, die die meisten PHP-Deployments seit 2010 betrieben hat.
Die architektonische Verschiebung ist real. In einem traditionellen Stack wandert eine HTTP-Anfrage vom Load Balancer zu Nginx, dann über einen Unix-Socket oder TCP zu einem PHP-FPM-Pool, wo ein Worker-Prozess ausgewählt wird, deine Anwendung bootet, den Request verarbeitet, eine Antwort durch FPM und Nginx zurücksendet und abbaut. Das FastCGI-Protokoll leistet bei jedem Request echte Arbeit. Statische Assets werden von Nginx bereitgestellt, was zwei Konfigurationsstücke bedeutet - eines für das Framework, eines für die Datei-Routes - und zwei Binaries, die unabhängig voneinander aktualisiert werden müssen.
FrankenPHP kollabiert diese Pipeline. Die gleiche Binary beendet TLS, stellt statische Dateien bereit und leitet dynamische Requests an eingebettetes PHP weiter. Es gibt keinen Socket, keine FastCGI-Übersetzung und keinen zweiten Daemon zum Supervisen. Caddys TLS-Automatisierung funktioniert sofort, einschließlich für Let's Encrypt und ZeroSSL. HTTP/3 (QUIC) wird durch Hinzufügen einer einzelnen Konfigurationszeile aktiviert.
Die andere Änderung, und die interessantere für die Performance, ist der Worker-Mode. Im klassischen Modus verhält sich FrankenPHP wie FPM: jeder Request bootet das Framework, führt den Controller aus und baut den Kernel ab. Im Worker-Mode bootet FrankenPHP deine Anwendung einmal, hält sie im Speicher und leitet jeden eingehenden Request an den warmen Prozess weiter. Für eine Symfony-Anwendung bedeutet das, dass der Container, der Route-Cache, die Doctrine-Metadaten und jede Service-Instanz zwischen Requests erhalten bleiben. Die Kosten des Bootstrappings verschieben sich von "jeder Request" zu "jedes Deployment".
Worker-Mode und was er für Symfony bedeutet
Das Symfony-Team unterstützt FrankenPHP-Worker-Mode offiziell durch einen kleinen Runtime-Adapter, und die Integration ist durch 2025 und Anfang 2026 erheblich gereift. Für eine typische Symfony 6.4 oder 7.x Anwendung ist das Aktivieren des Worker-Modes ein paar Zeilen Code plus ein sorgfältiges Audit der Services, die Request-gebundenen Zustand halten.
Ein minimaler Worker-Einstiegspunkt sieht so aus:
<?php
// public/index.php
use App\Kernel;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};
Die Runtime weiß, dass sie sich im Worker-Mode befindet, durch die FRANKENPHP_CONFIG-Umgebungsvariable, und das runtime/frankenphp-symfony-Paket verwaltet die Request-Dispatch-Schleife. Aus der Perspektive der Anwendung sehen Controller und Services identisch zu FPM aus. Der Unterschied ist, was zwischen Requests überlebt.
Für eine echte Symfony-API, die JSON bereitstellt, ist das Performance-Delta beträchtlich. Auf einem typischen CRUD-Endpoint mit Doctrine und einem Twig-freien Response-Pfad liefert der Worker-Mode typischerweise drei bis fünf Mal mehr Durchsatz als FPM, wobei die P95-Latenz um 40 bis 60 Prozent sinkt. Der Symfony-Framework-Boot selbst kann leicht 30 ms pro Request ausmachen, und der Worker-Mode gewinnt diese Zeit zurück.
Der Trade-off ist, dass alles, was du in eine statische Property, einen Service-Konstruktor oder den kompilierten Zustand des Containers steckst, für immer lebt. Das ist in Ordnung für zustandslose Services und Konfiguration. Es ist nicht in Ordnung für einen Doctrine EntityManager, der über eine Stunde Traffic 50.000 verwaltete Entities angesammelt hat, oder einen Logger, der jeden Request-Body für "Debugging" gepuffert hat.
Die konkreten Symfony-Services, die Aufmerksamkeit benötigen, sind vorhersagbar: der Entity Manager (Doctrine liefert einen clearEntityManagerSubscriber, den du aktivieren solltest), der Request-Stack (reset sich bereits korrekt), Monolog-Handler, die puffern (verwende den FingersCrossedHandler sorgfältig), und benutzerdefinierte Services, die Zustand über RequestEvent-Lifecycle-Hooks halten. Der kernel.reset-Tag existiert genau dafür - Services, die damit getaggt sind, erhalten ihren Zustand zwischen Requests geleert, und dieser Vertrag wird im Worker-Mode lastentragend.
Migration von Nginx + PHP-FPM
Eine FrankenPHP-Migration auf einer Symfony-Produktionsanwendung wird am besten als drei sequentielle Phasen behandelt, nicht als ein großer Switch. Jede Phase ist unabhängig reversibel.
Phase 1: FrankenPHP im klassischen Modus ausführen
Die erste Phase ersetzt Nginx und PHP-FPM durch FrankenPHP im klassischen (Request-per-Process) Modus. Es sind keine Anwendungsänderungen erforderlich. Das Dockerfile wird einfacher:
FROM dunglas/frankenphp:1.4-php8.3 AS base
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --no-progress
COPY . .
RUN composer install --no-dev --optimize-autoloader \
&& bin/console cache:warmup --env=prod
ENV SERVER_NAME=:80 \
APP_ENV=prod
CMD ["frankenphp", "run", "--config", "/etc/caddy/Caddyfile"]
Ein minimales Caddyfile:
{
frankenphp
order php_server before file_server
}
:80 {
root * /app/public
encode zstd br gzip
php_server
}
Diese Phase sollte funktional ein No-Op sein. Führe das einige Tage im Produktionsbetrieb aus. Wenn etwas schiefgehen soll - meistens ein hardcodierter $_SERVER-Wert oder ein Nginx-spezifischer Header-Trick - wird es hier auftauchen, in einem Kontext, in dem ein Rollback trivial ist.
Phase 2: Services für Reset auditieren und taggen
Bevor du den Worker-Mode aktivierst, gehe durch bin/console debug:container --tag=kernel.reset und bestätige, dass jeder Service mit veränderlichem Zustand entweder zwischen Requests zurückgesetzt wird oder wirklich für das Caching gedacht ist. Das Audit produziert typischerweise eine kurze Liste: ein session-bezogener Decorator, ein Audit-Log-Puffer, ein Tenant-Context-Resolver. Diese mit kernel.reset zu taggen und die reset()-Methode zu implementieren dauert einen Nachmittag.
PHPStan mit der kernel.reset-Regel fängt die häufigsten Fehler ab (ein Service, der Zustand mutiert, aber keinen Reset-Hook deklariert). Es als CI-Check laufen zu lassen ist die günstigste mögliche Versicherung.
Phase 3: Worker-Mode aktivieren
Die finale Phase aktiviert den Worker-Mode durch Caddyfile-Konfiguration:
{
frankenphp {
worker {
file /app/public/index.php
num 4
env APP_RUNTIME Runtime\\FrankenPhpSymfony\\Runtime
}
}
}
:80 {
root * /app/public
encode zstd br gzip
php_server
}
Die num 4-Direktive entspricht der Anzahl der CPU-Kerne, die dem Container zur Verfügung stehen. Mit vier warmen Symfony-Kernels pro Pod verarbeitet ein typischer 2-vCPU-Produktions-Container ungefähr den Durchsatz eines 8-vCPU-Pods mit FPM. Pod-Sizing und Autoscaling-Regeln müssen neu abgestimmt werden - was früher CPU-gebunden war, ist jetzt speichergebunden.
Operative Besonderheiten, die Hype-Posts überspringen
Speicherwachstum ist real
PHP-Prozesse waren nie für ewiges Leben ausgelegt. Ein Worker-Mode-Kernel, der Millionen von Requests ausführt, wird Speicher aus einem Dutzend Quellen ansammeln: Opcache-Verdichtung, Vendor-Bibliotheken mit eigenen Caches, gelegentliche Leaks in Extensions wie Imagick oder gRPC, und Doctrines Identity Map, wenn etwas den Reset-Hook umgeht.
Die Abschwächung ist die max_requests-Direktive, die einen Worker nach einer fixen Anzahl von Requests neu startet:
worker {
file /app/public/index.php
num 4
max_requests 1000
}
Ein Wert zwischen 500 und 5.000 funktioniert für die meisten Anwendungen. Neustarts sind graceful - laufende Requests werden auf dem bestehenden Worker abgeschlossen, während ein neuer bootet - sodass der Latenz-Impact unsichtbar ist.
Deployment wird zu einem echten Ereignis
Bei FPM bedeutete Deployment, Dateien zu synchon und den FPM-Master neu zu laden. Der neue Code trat beim nächsten Request in Kraft. Mit Worker-Mode werden Dateiänderungen erst nach einem Worker-Neustart wirksam. Das Deployment-Mechanismus ist daher ein graceful FrankenPHP-Neustart, den Caddy nativ unterstützt (SIGUSR1).
Für containerisierte Deployments funktioniert das standardmäßige Rolling-Update-Muster gut - Kubernetes dreht einen neuen Pod hoch, leert Traffic vom alten und fährt ihn herunter. Stelle nur sicher, dass der Readiness-Probe wartet, bis der Worker-Pool vollständig gestartet ist, was bei einer moderat komplexen Symfony-Anwendung 2-4 Sekunden dauern kann.
Observability muss den Prozess abdecken
APM-Tools, die sich in den Request-Lifecycle-Pro-Request von FPM eingehängt haben, benötigen FrankenPHP-fähige Agent-Updates. New Relic, Datadog und Tideways liefern ab 2025 alle FrankenPHP-fähige Agents, aber zu überprüfen, dass Traces die Request-Grenzen korrekt übertragen, ist es wert, am ersten Tag zu tun statt beim ersten Vorfall.
Einige Bundles holen noch auf
Die meisten Mainstream-Symfony-Bundles behandeln den Worker-Mode heute korrekt. Einige - insbesondere ältere Payment-Integrationen, benutzerdefinierte CMS-Bundles und alles, was globale PHP-Error-Handler registriert - nehmen einen frischen Prozess pro Request an und benötigen entweder einen Patch oder einen Service-Tag. Die symfony/runtime-Dokumentation pflegt eine Liste bekannt-guter und bekannt-schlechter Bundle-Versionen.
Wann FrankenPHP sich lohnt - und wann nicht
Für eine stark frequentierte Symfony-API, eine SaaS-Anwendung mit vorhersehbaren Request-Shapes oder alles, das echte Zeit im Framework-Boot verbringt, ist FrankenPHP mit Worker-Mode einer der größten Performance-Gewinne ohne Code-Rewrite. Die Migration ist begrenzt, der Rollback ist während der klassischen-Modus-Phase trivial und die operative Vereinfachung, eine Binary statt zwei zu betreiben, ist für kleine Teams wertvoll.
Für eine kleine Marketing-Website mit ein paar hundert Requests pro Stunde sind die Gewinne meist kosmetisch. Für eine Symfony-Anwendung, die auf Bundles aufgebaut ist, die nicht für den Worker-Mode auditiert wurden, ist die Aufräumarbeit echter Engineering-Aufwand. Und für Teams, die stark auf per-Request-PHP-Error-Handler oder prozessualen Zustand angewiesen sind, passt das Modell ungünstig.
FrankenPHP ist eine ernsthafte Modernisierung für die richtige Art von Anwendung, kein universelles Upgrade. Die Teams, die am meisten davon profitieren, sind meistens diejenigen, die bereits in sauberes Service-Design investiert haben, ihre Symfony-Version aktuell gehalten haben und Observability haben, um die Änderung zu validieren.
Wenn dein Team eine FrankenPHP-Migration abwägt - oder eine Symfony-Anwendung trägt, die ihren Deployment-Stack jahrelang nicht gesehen hat - ist das genau die Art von Arbeit, die Wolf-Tech für europäische SaaS- und Produktunternehmen ausführt. Ein fokussiertes Code-Qualitäts-Audit identifiziert typischerweise die Worker-Mode-Blocker vor dem Beginn der Migration, und die Legacy-Code-Optimierung und Tech-Stack-Strategie verwandeln "Wir sollten FrankenPHP ausprobieren" in ein durchdachtes Engineering-Rollout. Kontaktiere uns unter hello@wolf-tech.io oder besuche wolf-tech.io für eine kostenlose Beratung, ob FrankenPHP der richtige nächste Schritt für deinen Stack ist.

