FrankenPHP — Moderner PHP-Application-Server auf Caddy
Praxis-Guide zu FrankenPHP — dem modernen PHP-Application-Server auf Caddy-Basis: Worker-Modus, automatisches HTTPS, HTTP/3 und Early Hints, ohne PHP-FPM.
FrankenPHP ist ein moderner PHP-Application-Server auf Basis des Caddy-Webservers: Die PHP-Laufzeit ist direkt in eine einzige Go-Binary eingebettet, ein separater PHP-FPM-Prozess entfällt also komplett. Das Herzstück ist der Worker-Modus — deine Anwendung bootstrappt nur einmal und bedient anschließend in einem langlebigen Prozess tausende Requests, was den Overhead pro Anfrage drastisch senkt. Dazu kommen automatisches HTTPS, HTTP/2 und HTTP/3 ab Werk sowie Early Hints (103), mit denen der Browser schon Assets lädt, während deine Antwort noch entsteht. Dieser Guide führt dich durch Installation, Caddyfile, Worker-Modus und Docker-Deployment.
Installation
curl -L https://github.com/dunglas/frankenphp/releases/latest/download/frankenphp-linux-x86_64 -o /usr/local/bin/frankenphp && chmod +x /usr/local/bin/frankenphp — Lädt die aktuelle statische FrankenPHP-Binary für Linux (x86_64) herunter und installiert sie.
curl -L https://github.com/dunglas/frankenphp/releases/latest/download/frankenphp-linux-x86_64 -o /usr/local/bin/frankenphpfrankenphp version — Zeigt die installierte FrankenPHP- und Caddy-Version an.
frankenphp versionfrankenphp list-modules — Listet alle verfügbaren Caddy- und FrankenPHP-Module auf.
frankenphp list-modulesfrankenphp upgrade — Aktualisiert FrankenPHP an Ort und Stelle auf die neueste Version.
frankenphp upgradeCLI-Kommandos
frankenphp run — Startet FrankenPHP im Vordergrund mit dem Caddyfile im aktuellen Verzeichnis.
frankenphp runfrankenphp run --config <file> — Startet FrankenPHP im Vordergrund mit einem bestimmten Caddyfile.
frankenphp run --config /etc/frankenphp/Caddyfilefrankenphp start — Startet FrankenPHP als Hintergrund-Daemon.
frankenphp startfrankenphp stop — Stoppt den laufenden FrankenPHP-Daemon.
frankenphp stopfrankenphp reload — Lädt die Caddyfile-Konfiguration ohne Ausfallzeit neu.
frankenphp reloadfrankenphp validate --config <file> — Prüft ein Caddyfile auf Fehler, ohne den Server zu starten.
frankenphp validate --config Caddyfilefrankenphp fmt --overwrite <file> — Formatiert ein Caddyfile nach dem kanonischen Stil und überschreibt es direkt.
frankenphp fmt --overwrite Caddyfilefrankenphp php-cli <script> — Führt ein PHP-Skript über die Kommandozeile mit der eingebetteten PHP-Laufzeit aus.
frankenphp php-cli artisan migratefrankenphp php-cli -r '<code>' — Führt PHP-Code direkt über die Kommandozeile aus.
frankenphp php-cli -r 'echo phpversion();'frankenphp php-server — Startet einen schnellen PHP-Entwicklungsserver im aktuellen Verzeichnis (wie php -S, nur besser).
frankenphp php-serverfrankenphp php-server --listen :8080 — Startet einen PHP-Entwicklungsserver auf einem bestimmten Port.
frankenphp php-server --listen :8080Dienstverwaltung (systemctl)
systemctl start frankenphp — Startet den FrankenPHP-Dienst.
systemctl start frankenphpsystemctl stop frankenphp — Stoppt den FrankenPHP-Dienst.
systemctl stop frankenphpsystemctl restart frankenphp — Startet FrankenPHP neu (unterbricht kurz die Verbindungen).
systemctl restart frankenphpsystemctl reload frankenphp — Lädt die Caddyfile-Konfiguration ohne Ausfallzeit neu.
systemctl reload frankenphpsystemctl status frankenphp — Zeigt den Status des FrankenPHP-Dienstes und die jüngsten Log-Ausgaben.
systemctl status frankenphpsystemctl enable frankenphp — Richtet FrankenPHP so ein, dass es automatisch beim Systemstart startet.
systemctl enable frankenphpjournalctl -u frankenphp -f — Verfolgt die FrankenPHP-Logs in Echtzeit über das systemd-Journal.
journalctl -u frankenphp -fCaddyfile: php_server & php
Die php_server-Direktive ist FrankenPHPs wichtigste Kurzform. Sie verarbeitet PHP-Dateien, statische Dateien und try_files in einer einzigen Direktive. TLS läuft automatisch.
example.com {
root * /var/www/html
php_server
}PHP-Site mit aktivierter gzip/zstd-Komprimierung.
example.com {
root * /var/www/html
encode gzip zstd
php_server
}php_server mit Optionen: explizite Index-Datei und Symlink-Auflösung für den Root.
php_server {
index index.php
resolve_root_symlink
}Nutzt die tiefergreifende php-Direktive für feingranulare Kontrolle über die Request-Verarbeitung.
example.com {
root * /var/www/html
php {
try_files {path} index.php
}
file_server
}Aktiviert den Worker-Modus: startet ein persistentes PHP-Worker-Skript neben dem Server.
example.com {
root * /var/www/html
php_server {
worker worker.php
}
}Worker-Modus
Startet einen einzelnen PHP-Worker-Prozess. Das Skript bootstrappt einmal und bedient alle Requests.
php_server {
worker /var/www/html/worker.php
}Startet 4 parallele PHP-Worker-Prozesse zur Bearbeitung gleichzeitiger Requests.
php_server {
worker {
file /var/www/html/worker.php
num 4
}
}Bestimmt die Anzahl der Worker automatisch anhand der verfügbaren CPU-Kerne.
php_server {
worker {
file /var/www/html/worker.php
num auto
}
}frankenphp_handle_request() — Worker-Skript-Muster: die Anwendung einmal außerhalb der Schleife bootstrappen, dann frankenphp_handle_request() in einer Schleife aufrufen. Die Closure läuft für jeden eingehenden Request.
<?php
// worker.php — bootstrap once
require __DIR__ . '/vendor/autoload.php';
$app = require __DIR__ . '/bootstrap/app.php';
// Handle requests in a loop until the server shuts down
while (frankenphp_handle_request(function () use ($app) {
$kernel = $app->make(Kernel::class);
$response = $kernel->handle(
$request = Request::capture()
);
$response->send();
$kernel->terminate($request, $response);
}));while (frankenphp_handle_request(fn() => handleRequest()));frankenphp_finish_request(); — Sendet die aktuelle HTTP-Antwort sofort an den Client und führt anschließend PHP-Code im Hintergrund weiter aus. Nützlich für die Nachbearbeitung nach der Antwort.
frankenphp_finish_request();
// background work here...Gib false aus dem Callback zurück, um den Worker-Prozess sauber herunterzufahren.
frankenphp_handle_request(function(): bool {
// return false to stop the worker
return true;
});Docker
docker run -v $PWD:/app/public -p 80:80 -p 443:443 dunglas/frankenphp — Liefert PHP-Dateien aus dem aktuellen Verzeichnis mit automatischem HTTPS aus (über die interne CA auf localhost).
docker run -v $PWD:/app/public -p 80:80 -p 443:443 dunglas/frankenphpdocker run -e SERVER_NAME=example.com -v $PWD:/app/public -p 80:80 -p 443:443 dunglas/frankenphp — Liefert eine Domain mit automatischem Let's-Encrypt-TLS aus, indem die Umgebungsvariable SERVER_NAME gesetzt wird.
docker run -e SERVER_NAME=example.com ... dunglas/frankenphpdocker run -e SERVER_NAME=:80 -v $PWD:/app/public -p 80:80 dunglas/frankenphp — Läuft in Docker nur über HTTP (ohne TLS), indem SERVER_NAME auf einen Port gesetzt wird.
docker run -e SERVER_NAME=:80 -v $PWD:/app/public -p 80:80 dunglas/frankenphpMinimales Dockerfile, um ein eigenständiges Image aus einem PHP-Projekt zu bauen.
FROM dunglas/frankenphp
COPY . /app/publicProduction-Dockerfile: Composer-Abhängigkeiten installieren und die Domain setzen.
FROM dunglas/frankenphp
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --optimize-autoloader
COPY . .
ENV SERVER_NAME=example.comFROM dunglas/frankenphp:latest-alpine — Nutzt das Alpine-basierte Image für eine kleinere Production-Image-Größe.
FROM dunglas/frankenphp:latest-alpinedocker exec -it <container> frankenphp php-cli artisan migrate — Führt ein PHP-CLI-Kommando in einem laufenden FrankenPHP-Container aus.
docker exec -it myapp frankenphp php-cli artisan migrateUmgebungsvariablen
SERVER_NAME=example.com — Setzt den Domainnamen. FrankenPHP stellt automatisch ein TLS-Zertifikat dafür bereit.
SERVER_NAME=example.com frankenphp runSERVER_NAME=:80 — Lauscht nur über HTTP ohne TLS (nützlich in der lokalen Entwicklung oder hinter einem TLS-terminierenden Proxy).
SERVER_NAME=:80 frankenphp runSERVER_NAME=localhost — Nutzt ein lokal vertrauenswürdiges Zertifikat aus Caddys interner CA für die lokale Entwicklung.
SERVER_NAME=localhost frankenphp runFRANKENPHP_CONFIG='worker ./worker.php' — Inline-Caddyfile-Snippet für den php_server-Block per Umgebungsvariable.
FRANKENPHP_CONFIG='worker ./worker.php' frankenphp runCADDY_GLOBAL_OPTIONS='debug' — Schleust globale Caddyfile-Optionen per Umgebungsvariable ein (z. B. Debug-Logging aktivieren).
CADDY_GLOBAL_OPTIONS='debug' frankenphp runXDG_DATA_HOME=/data XDG_CONFIG_HOME=/config — Überschreibt die Daten- und Konfigurationsverzeichnisse von FrankenPHP/Caddy (Standard: ~/.local/share und ~/.config).
XDG_DATA_HOME=/data XDG_CONFIG_HOME=/config frankenphp runFramework-Integration
Liefert eine Standard-Laravel-App aus (ohne Worker-Modus). Das Document-Root ist das public/-Verzeichnis.
example.com {
root * /var/www/laravel/public
php_server
}Laravel mit FrankenPHP-Worker-Modus über Laravel Octane.
example.com {
root * /var/www/laravel/public
php_server {
worker {
file /var/www/laravel/public/frankenphp-worker.php
num 4
}
}
}Installiert und startet Laravel Octane mit FrankenPHP als Server-Treiber.
composer require laravel/octane
php artisan octane:install --server=frankenphp
php artisan octane:frankenphpNutzt die FrankenPHP-Symfony-Runtime für den konfigurationsfreien Symfony-Worker-Modus.
composer require runtime/frankenphp-symfony
APP_RUNTIME=Runtime\FrankenPhpSymfony\Runtime php -S localhost:8000 public/index.phpLiefert eine WordPress-Site aus. FrankenPHP ersetzt sowohl Apache/nginx als auch PHP-FPM.
example.com {
root * /var/www/wordpress
php_server
encode gzip zstd
}Liefert eine Drupal-Site aus. Setze root auf das web/-Unterverzeichnis.
example.com {
root * /var/www/drupal/web
php_server
}TLS & HTTPS
TLS läuft für öffentliche Domains vollautomatisch — keine zusätzliche Konfiguration nötig.
example.com {
root * /app/public
php_server
# TLS provisioned automatically
}tls /path/to/cert.pem /path/to/key.pem — Nutzt ein manuell verwaltetes TLS-Zertifikat statt des automatischen.
tls /etc/ssl/certs/example.crt /etc/ssl/private/example.keyfrankenphp trust — Installiert FrankenPHPs lokale Root-CA in die Vertrauensspeicher von System und Browser für HTTPS auf localhost.
frankenphp trustfrankenphp untrust — Entfernt die lokale Root-CA aus den Vertrauensspeichern.
frankenphp untrustSetzt die ACME-Registrierungs-E-Mail im globalen Optionsblock oberhalb aller Site-Blöcke.
{
email admin@example.com
}
example.com {
root * /app/public
php_server
}Early Hints (103)
header('Link: </style.css>; rel=preload; as=style', false); — Sendet eine HTTP-103-Early-Hints-Antwort aus PHP, indem header() vor jeder Ausgabe aufgerufen wird. FrankenPHP leitet sie sofort an den Client weiter.
header('Link: </app.js>; rel=preload; as=script', false);Sendet mehrere Early Hints, bevor die vollständige Antwort erzeugt wird. Browser können bereits Assets laden, während der Server noch arbeitet.
header('Link: </image.jpg>; rel=preload; as=image', false);
header('Link: </style.css>; rel=preload; as=style', false);
// ... generate response ...
echo $html;Logging & Debugging
Schreibt Access-Logs im JSON-Format in eine Datei.
example.com {
log {
output file /var/log/frankenphp/access.log
format json
}
php_server
}Aktiviert ausführliches Debug-Logging im globalen Optionsblock.
{
debug
}
example.com {
php_server
}journalctl -u frankenphp -f — Verfolgt die FrankenPHP-Logs in Echtzeit über das systemd-Journal.
journalctl -u frankenphp -fcurl -s http://localhost:2019/config/ | jq '.' — Inspiziert die aktuelle FrankenPHP/Caddy-Konfiguration über die Admin-API.
curl -s http://localhost:2019/config/ | jq '.'curl -s http://localhost:2019/reverse_proxy/upstreams — Prüft den Health-Status der Upstreams über die Admin-API.
curl -s http://localhost:2019/reverse_proxy/upstreams | jq '.'Praxisbeispiele
Production-taugliche PHP-Site mit Komprimierung und gehärteten Security-Headern.
example.com {
root * /var/www/app/public
encode gzip zstd
php_server
header -Server
header X-Frame-Options DENY
header X-Content-Type-Options nosniff
}Production-Deployment im Worker-Modus mit automatisch dimensioniertem Worker-Pool und Komprimierung.
example.com {
root * /var/www/app/public
php_server {
worker {
file /var/www/app/public/worker.php
num auto
}
}
encode gzip zstd
}Leitet www auf die Apex-Domain um und liefert die PHP-App aus.
www.example.com {
redir https://example.com{uri} permanent
}
example.com {
root * /var/www/app/public
php_server
}Gehärtetes Production-Setup: Admin-API deaktiviert, ACME-E-Mail gesetzt, Logging in Datei.
{
admin off
email admin@example.com
}
example.com {
root * /app/public
encode gzip zstd
php_server
log {
output file /var/log/frankenphp/access.log
}
}docker run --rm -p 8080:8080 -e SERVER_NAME=:8080 -v $PWD:/app/public dunglas/frankenphp — Liefert das aktuelle Verzeichnis schnell als PHP-App auf Port 8080 ohne TLS aus.
docker run --rm -p 8080:8080 -e SERVER_NAME=:8080 -v $PWD:/app/public dunglas/frankenphp Fazit
FrankenPHP fasst den klassischen Stack aus Webserver und PHP-FPM in einer einzigen, eigenständigen Binary zusammen und macht PHP per Worker-Modus zu einem durchsatzstarken Application-Server, der bei der Request-Latenz mit Node.js oder Go mithält. Der Preis dafür ist Zustand: Weil dein Code zwischen den Requests im Speicher bleibt, musst du auf Memory-Leaks und auf State achten, der von einem Request in den nächsten überläuft — halte deine Request-Handler sauber und setze globalen Zustand in der Schleife zurück. Wie bei Caddy läuft HTTPS vollautomatisch, sodass dich von einem Production-Deployment oft nur ein Domainname und eine php_server-Direktive trennen.
Weiterführende Links
- FrankenPHP – offizielle Dokumentation – Installation, Worker-Modus und Konfigurationsreferenz (englisch)
- FrankenPHP auf GitHub – Quellcode, Releases und Issue-Tracker (englisch)