FrankenPHP in Production: Worker Mode, Embedded Binaries, and Real Performance
A practical guide to running FrankenPHP in production. Worker mode, embedded binaries, HTTP/3, and the deployment patterns that actually pay off.
For twenty-plus years, “deploying PHP” meant the same uneasy triangle: Apache or Nginx in front, PHP-FPM behind, and a configuration file glued between them that nobody on the team really understood. FrankenPHP is the first genuinely different answer that stack has had in a long time, and after a couple of years of steady releases it has earned serious consideration for production workloads. If you have not looked at it since the 1.0 release, the project has moved considerably — worker mode is rock solid, embedded binaries have shipped for real apps, and the Laravel and Symfony integrations are boring in all the right ways.
Here is what actually matters when you put FrankenPHP in front of a real application.
Why FrankenPHP Exists
FrankenPHP is an application server for PHP built on top of the Caddy web server, written in Go, with PHP embedded as a library rather than executed as a separate process. That architectural choice is the whole story. Because PHP runs inside Caddy’s process, there is no FastCGI hop, no socket negotiation, no separate lifecycle to monitor. You get a single binary that serves HTTP/1, HTTP/2, and HTTP/3 with automatic HTTPS, and it happens to run your PHP app.
The headline benefit is performance, but the real benefit is operational simplicity. One process, one config file, one health check. In a world where teams spend weeks wiring up Kubernetes probes for Nginx, PHP-FPM, and a sidecar, FrankenPHP’s single-binary model is a genuine relief.
Worker Mode Is the Point
Traditional PHP boots your framework on every request. For Laravel or Symfony, that means loading hundreds of classes, building the container, booting service providers — all so you can handle a request that might complete in two milliseconds of actual business logic. Worker mode fixes this by keeping the framework in memory between requests.
In a Laravel project, enabling worker mode is shockingly small:
FROM dunglas/frankenphp
ENV SERVER_NAME=":80"
ENV FRANKENPHP_CONFIG="worker ./public/index.php"
COPY . /app
WORKDIR /app
RUN composer install --no-dev --optimize-autoloader
The worker directive tells FrankenPHP to keep public/index.php booted. Laravel ships with first-class support via the laravel/octane package configured for FrankenPHP:
composer require laravel/octane
php artisan octane:install --server=frankenphp
php artisan octane:start --server=frankenphp --workers=4
Symfony has equivalent support via the runtime component and the php-runtime/frankenphp-symfony package. In both frameworks, expect 3x to 10x throughput gains on CPU-bound endpoints, and the numbers climb higher for apps that do heavy bootstrapping.
The catch, and this is important, is that worker mode exposes every piece of global state you have been getting away with. Static properties persist across requests. Container bindings persist. If you mutate a singleton, the next request sees your changes. Most Laravel and Symfony code is fine — the frameworks handle request/response isolation — but your own code needs a review. Octane’s documentation has a concise checklist; read it before you ship.
Embedded Binaries for Real Distribution
FrankenPHP’s embed feature packages your entire PHP application, including the runtime, into a single static binary. You end up with one file you can drop on a server, a laptop, or inside a container, and it runs your app.
# From the FrankenPHP repo with your app in the `app/` directory
docker buildx bake --load static-builder-gnu
./dist/frankenphp-linux-x86_64 php-server --root app/public
For internal tools, self-hosted SaaS products, or any scenario where “just give me an installer” is a selling point, this is a game changer. The Symfony CLI has been shipping FrankenPHP-based binaries for a while, and the workflow scales beyond small apps.
Keep in mind that embedded binaries include your secrets if you are not careful. Use environment variables at runtime, not baked-in config, and treat the resulting binary as public code even when it is distributed privately.
HTTP/3, Early Hints, and Mercure
Because Caddy already speaks the modern web, FrankenPHP inherits features that are genuinely painful to add to a PHP-FPM setup. HTTP/3 is on by default when you have a valid TLS cert. Early Hints (HTTP 103) let you push CSS and critical resources to the browser while PHP is still building the response — the Laravel helpers landed in 11.x and Symfony’s WebLink component has supported it for a while.
Mercure, a protocol for server-sent events and real-time updates, is built into FrankenPHP. For dashboards, notifications, or live-updating UIs where you do not want the complexity of WebSockets, this is the lightest-weight option in the PHP ecosystem right now. Publishing an update is a single HTTP POST from your worker.
use Symfony\Component\Mercure\Update;
$update = new Update(
'https://example.com/orders/42',
json_encode(['status' => 'shipped'])
);
$hub->publish($update);
What to Watch For
Worker mode changes the game for performance but also for reliability. A memory leak that was invisible when PHP-FPM recycled processes every 500 requests becomes a slow bleed that takes down a worker over a weekend. Set max_requests on each worker to recycle them periodically. Monitor memory per worker. Use opcache.validate_timestamps=0 in production and deploy by replacing the container, not by editing files in place.
Also, FrankenPHP is not a drop-in replacement for every Nginx config. If you have years of carefully tuned rewrite rules, static asset handling, or Fastly-style caching headers, budget time to port them to Caddyfile syntax. The Caddy config language is lovely once you know it, but it is not Nginx.
Is It Ready?
Yes, with eyes open. Shopware has been running FrankenPHP in production for more than a year. Symfony’s CLI ships with it embedded. Major Laravel hosting providers now list it as a first-tier runtime alongside Octane with Swoole. The ecosystem extensions, monitoring integrations, and Docker images are mature.
If you run a small-to-medium PHP app and want to squeeze more performance out of your infrastructure without rewriting anything, spin up a FrankenPHP container this week and benchmark it against your current stack. The worst case is you learn something; the best case is you delete three layers of infrastructure and sleep better at night.