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/frankenphp

frankenphp version — Zeigt die installierte FrankenPHP- und Caddy-Version an.

frankenphp version

frankenphp list-modules — Listet alle verfügbaren Caddy- und FrankenPHP-Module auf.

frankenphp list-modules

frankenphp upgrade — Aktualisiert FrankenPHP an Ort und Stelle auf die neueste Version.

frankenphp upgrade

CLI-Kommandos

frankenphp run — Startet FrankenPHP im Vordergrund mit dem Caddyfile im aktuellen Verzeichnis.

frankenphp run

frankenphp run --config <file> — Startet FrankenPHP im Vordergrund mit einem bestimmten Caddyfile.

frankenphp run --config /etc/frankenphp/Caddyfile

frankenphp start — Startet FrankenPHP als Hintergrund-Daemon.

frankenphp start

frankenphp stop — Stoppt den laufenden FrankenPHP-Daemon.

frankenphp stop

frankenphp reload — Lädt die Caddyfile-Konfiguration ohne Ausfallzeit neu.

frankenphp reload

frankenphp validate --config <file> — Prüft ein Caddyfile auf Fehler, ohne den Server zu starten.

frankenphp validate --config Caddyfile

frankenphp fmt --overwrite <file> — Formatiert ein Caddyfile nach dem kanonischen Stil und überschreibt es direkt.

frankenphp fmt --overwrite Caddyfile

frankenphp php-cli <script> — Führt ein PHP-Skript über die Kommandozeile mit der eingebetteten PHP-Laufzeit aus.

frankenphp php-cli artisan migrate

frankenphp 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-server

frankenphp php-server --listen :8080 — Startet einen PHP-Entwicklungsserver auf einem bestimmten Port.

frankenphp php-server --listen :8080

Dienstverwaltung (systemctl)

systemctl start frankenphp — Startet den FrankenPHP-Dienst.

systemctl start frankenphp

systemctl stop frankenphp — Stoppt den FrankenPHP-Dienst.

systemctl stop frankenphp

systemctl restart frankenphp — Startet FrankenPHP neu (unterbricht kurz die Verbindungen).

systemctl restart frankenphp

systemctl reload frankenphp — Lädt die Caddyfile-Konfiguration ohne Ausfallzeit neu.

systemctl reload frankenphp

systemctl status frankenphp — Zeigt den Status des FrankenPHP-Dienstes und die jüngsten Log-Ausgaben.

systemctl status frankenphp

systemctl enable frankenphp — Richtet FrankenPHP so ein, dass es automatisch beim Systemstart startet.

systemctl enable frankenphp

journalctl -u frankenphp -f — Verfolgt die FrankenPHP-Logs in Echtzeit über das systemd-Journal.

journalctl -u frankenphp -f

Caddyfile: 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/frankenphp

docker 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/frankenphp

docker 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/frankenphp

Minimales Dockerfile, um ein eigenständiges Image aus einem PHP-Projekt zu bauen.

FROM dunglas/frankenphp
COPY . /app/public

Production-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.com

FROM dunglas/frankenphp:latest-alpine — Nutzt das Alpine-basierte Image für eine kleinere Production-Image-Größe.

FROM dunglas/frankenphp:latest-alpine

docker 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 migrate

Umgebungsvariablen

SERVER_NAME=example.com — Setzt den Domainnamen. FrankenPHP stellt automatisch ein TLS-Zertifikat dafür bereit.

SERVER_NAME=example.com frankenphp run

SERVER_NAME=:80 — Lauscht nur über HTTP ohne TLS (nützlich in der lokalen Entwicklung oder hinter einem TLS-terminierenden Proxy).

SERVER_NAME=:80 frankenphp run

SERVER_NAME=localhost — Nutzt ein lokal vertrauenswürdiges Zertifikat aus Caddys interner CA für die lokale Entwicklung.

SERVER_NAME=localhost frankenphp run

FRANKENPHP_CONFIG='worker ./worker.php' — Inline-Caddyfile-Snippet für den php_server-Block per Umgebungsvariable.

FRANKENPHP_CONFIG='worker ./worker.php' frankenphp run

CADDY_GLOBAL_OPTIONS='debug' — Schleust globale Caddyfile-Optionen per Umgebungsvariable ein (z. B. Debug-Logging aktivieren).

CADDY_GLOBAL_OPTIONS='debug' frankenphp run

XDG_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 run

Framework-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:frankenphp

Nutzt 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.php

Liefert 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.key

frankenphp trust — Installiert FrankenPHPs lokale Root-CA in die Vertrauensspeicher von System und Browser für HTTPS auf localhost.

frankenphp trust

frankenphp untrust — Entfernt die lokale Root-CA aus den Vertrauensspeichern.

frankenphp untrust

Setzt 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 -f

curl -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.

Verwandte Kommandos

  • apache – der etablierte Webserver, den FrankenPHP ersetzen kann
  • caddy – der Webserver, auf dem FrankenPHP aufbaut
  • certbot – TLS-Zertifikate manuell beziehen (FrankenPHP erledigt das automatisch)