# FrankenPHP — Modern PHP Application Server on Caddy

> Practical guide to FrankenPHP — the modern PHP app server built on Caddy: worker mode, automatic HTTPS, HTTP/3 and Early Hints, no PHP-FPM needed.

Source: https://www.jpkc.com/db/en/cheatsheets/web-servers/frankenphp/

<!-- PROSE:intro -->
FrankenPHP is a modern PHP application server built on top of the Caddy web server, with the PHP runtime embedded directly into a single Go binary — so you no longer need a separate PHP-FPM process. Its standout feature is worker mode: your application bootstraps once and then handles thousands of requests in a long-running process, cutting per-request overhead dramatically. On top of that you get automatic HTTPS, HTTP/2 and HTTP/3 out of the box, plus Early Hints (103) so the browser can start fetching assets before your response is ready. This guide walks you through installation, the Caddyfile, worker mode and Docker deployment.
<!-- PROSE:intro:end -->

## 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` — Download and install the latest FrankenPHP static binary on Linux (x86_64).

```bash
curl -L https://github.com/dunglas/frankenphp/releases/latest/download/frankenphp-linux-x86_64 -o /usr/local/bin/frankenphp
```

`frankenphp version` — Show the installed FrankenPHP and Caddy version.

```bash
frankenphp version
```

`frankenphp list-modules` — List all available Caddy and FrankenPHP modules.

```bash
frankenphp list-modules
```

`frankenphp upgrade` — Upgrade FrankenPHP to the latest release in-place.

```bash
frankenphp upgrade
```

## CLI Commands

`frankenphp run` — Start FrankenPHP in the foreground using the Caddyfile in the current directory.

```bash
frankenphp run
```

`frankenphp run --config <file>` — Start FrankenPHP in the foreground with a specific Caddyfile.

```bash
frankenphp run --config /etc/frankenphp/Caddyfile
```

`frankenphp start` — Start FrankenPHP as a background daemon.

```bash
frankenphp start
```

`frankenphp stop` — Stop the running FrankenPHP daemon.

```bash
frankenphp stop
```

`frankenphp reload` — Reload the Caddyfile configuration with zero downtime.

```bash
frankenphp reload
```

`frankenphp validate --config <file>` — Validate a Caddyfile for errors without starting the server.

```bash
frankenphp validate --config Caddyfile
```

`frankenphp fmt --overwrite <file>` — Format and overwrite a Caddyfile in place according to canonical style.

```bash
frankenphp fmt --overwrite Caddyfile
```

`frankenphp php-cli <script>` — Execute a PHP script from the command line using the embedded PHP runtime.

```bash
frankenphp php-cli artisan migrate
```

`frankenphp php-cli -r '<code>'` — Run inline PHP code from the command line.

```bash
frankenphp php-cli -r 'echo phpversion();'
```

`frankenphp php-server` — Start a quick PHP development server in the current directory (like php -S, but better).

```bash
frankenphp php-server
```

`frankenphp php-server --listen :8080` — Start a PHP development server on a specific port.

```bash
frankenphp php-server --listen :8080
```

## Service Management (systemctl)

`systemctl start frankenphp` — Start the FrankenPHP service.

```bash
systemctl start frankenphp
```

`systemctl stop frankenphp` — Stop the FrankenPHP service.

```bash
systemctl stop frankenphp
```

`systemctl restart frankenphp` — Restart FrankenPHP (briefly drops connections).

```bash
systemctl restart frankenphp
```

`systemctl reload frankenphp` — Reload the Caddyfile configuration with zero downtime.

```bash
systemctl reload frankenphp
```

`systemctl status frankenphp` — Show FrankenPHP service status and recent log output.

```bash
systemctl status frankenphp
```

`systemctl enable frankenphp` — Enable FrankenPHP to start automatically on system boot.

```bash
systemctl enable frankenphp
```

`journalctl -u frankenphp -f` — Follow FrankenPHP logs in real-time via the systemd journal.

```bash
journalctl -u frankenphp -f
```

## Caddyfile: php_server & php

The php_server directive is FrankenPHP's main shorthand. It handles PHP files, static files, and try_files in one directive. TLS is automatic.

```bash
example.com {
    root * /var/www/html
    php_server
}
```

PHP site with gzip/zstd compression enabled.

```bash
example.com {
    root * /var/www/html
    encode gzip zstd
    php_server
}
```

php_server with options: explicit index file and symlink resolution for the root.

```bash
php_server {
    index index.php
    resolve_root_symlink
}
```

Use the lower-level php directive for fine-grained control over request handling.

```bash
example.com {
    root * /var/www/html
    php {
        try_files {path} index.php
    }
    file_server
}
```

Enable worker mode: start a persistent PHP worker script alongside the server.

```bash
example.com {
    root * /var/www/html
    php_server {
        worker worker.php
    }
}
```

## Worker Mode

Start a single PHP worker process. The script bootstraps once and handles all requests.

```bash
php_server {
    worker /var/www/html/worker.php
}
```

Start 4 parallel PHP worker processes for handling concurrent requests.

```bash
php_server {
    worker {
        file /var/www/html/worker.php
        num 4
    }
}
```

Automatically determine the number of workers based on available CPU cores.

```bash
php_server {
    worker {
        file /var/www/html/worker.php
        num auto
    }
}
```

`frankenphp_handle_request()` — Worker script pattern: bootstrap the app once outside the loop, then call `frankenphp_handle_request()` in a loop. The closure runs for each incoming request.

```php
<?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);
}));
```

```bash
while (frankenphp_handle_request(fn() => handleRequest()));
```

`frankenphp_finish_request();` — Send the current HTTP response to the client immediately, then continue executing PHP code in the background. Useful for post-response processing.

```bash
frankenphp_finish_request();
// background work here...
```

Return false from the callback to gracefully shut down the worker process.

```bash
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` — Serve PHP files from the current directory with automatic HTTPS (using internal CA on localhost).

```bash
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` — Serve a domain with automatic Let's Encrypt TLS by setting the SERVER_NAME environment variable.

```bash
docker run -e SERVER_NAME=example.com ... dunglas/frankenphp
```

`docker run -e SERVER_NAME=:80 -v $PWD:/app/public -p 80:80 dunglas/frankenphp` — Run on HTTP only (no TLS) inside Docker by setting SERVER_NAME to a port.

```bash
docker run -e SERVER_NAME=:80 -v $PWD:/app/public -p 80:80 dunglas/frankenphp
```

Minimal Dockerfile to build a self-contained image from a PHP project.

```bash
FROM dunglas/frankenphp
COPY . /app/public
```

Production Dockerfile: install Composer dependencies and set the domain.

```bash
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` — Use the Alpine-based image for a smaller production image size.

```bash
FROM dunglas/frankenphp:latest-alpine
```

`docker exec -it <container> frankenphp php-cli artisan migrate` — Run a PHP CLI command inside a running FrankenPHP container.

```bash
docker exec -it myapp frankenphp php-cli artisan migrate
```

## Environment Variables

`SERVER_NAME=example.com` — Set the domain name. FrankenPHP automatically provisions a TLS certificate for it.

```bash
SERVER_NAME=example.com frankenphp run
```

`SERVER_NAME=:80` — Listen on HTTP only without TLS (useful in local dev or behind a TLS-terminating proxy).

```bash
SERVER_NAME=:80 frankenphp run
```

`SERVER_NAME=localhost` — Use a locally-trusted certificate from Caddy's internal CA for local development.

```bash
SERVER_NAME=localhost frankenphp run
```

`FRANKENPHP_CONFIG='worker ./worker.php'` — Inline Caddyfile snippet for the php_server block via environment variable.

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

`CADDY_GLOBAL_OPTIONS='debug'` — Inject global Caddyfile options via environment variable (e.g. enable debug logging).

```bash
CADDY_GLOBAL_OPTIONS='debug' frankenphp run
```

`XDG_DATA_HOME=/data XDG_CONFIG_HOME=/config` — Override FrankenPHP/Caddy data and config directories (default: ~/.local/share and ~/.config).

```bash
XDG_DATA_HOME=/data XDG_CONFIG_HOME=/config frankenphp run
```

## Framework Integration

Serve a standard Laravel app (without worker mode). The document root is the public/ directory.

```bash
example.com {
    root * /var/www/laravel/public
    php_server
}
```

Laravel with FrankenPHP worker mode using Laravel Octane.

```bash
example.com {
    root * /var/www/laravel/public
    php_server {
        worker {
            file /var/www/laravel/public/frankenphp-worker.php
            num 4
        }
    }
}
```

Install and start Laravel Octane with FrankenPHP as the server driver.

```bash
composer require laravel/octane
php artisan octane:install --server=frankenphp
php artisan octane:frankenphp
```

Use the FrankenPHP Symfony Runtime for zero-configuration Symfony worker mode.

```bash
composer require runtime/frankenphp-symfony
APP_RUNTIME=Runtime\FrankenPhpSymfony\Runtime php -S localhost:8000 public/index.php
```

Serve a WordPress site. FrankenPHP replaces both Apache/nginx and PHP-FPM.

```bash
example.com {
    root * /var/www/wordpress
    php_server
    encode gzip zstd
}
```

Serve a Drupal site. Point root to the web/ subdirectory.

```bash
example.com {
    root * /var/www/drupal/web
    php_server
}
```

## TLS & HTTPS

TLS is fully automatic for public domains — no extra configuration needed.

```bash
example.com {
    root * /app/public
    php_server
    # TLS provisioned automatically
}
```

`tls /path/to/cert.pem /path/to/key.pem` — Use a manually managed TLS certificate instead of the automatic one.

```bash
tls /etc/ssl/certs/example.crt /etc/ssl/private/example.key
```

`frankenphp trust` — Install FrankenPHP's local root CA into the system and browser trust stores for localhost HTTPS.

```bash
frankenphp trust
```

`frankenphp untrust` — Remove the local root CA from trust stores.

```bash
frankenphp untrust
```

Set the ACME registration email in the global options block above all site blocks.

```bash
{
    email admin@example.com
}
example.com {
    root * /app/public
    php_server
}
```

## Early Hints (103)

`header('Link: </style.css>; rel=preload; as=style', false);` — Send an HTTP 103 Early Hints response from PHP by calling header() before any output. FrankenPHP forwards it to the client immediately.

```bash
header('Link: </app.js>; rel=preload; as=script', false);
```

Send multiple Early Hints before generating the full response. Browsers can start fetching assets while the server is still processing.

```bash
header('Link: </image.jpg>; rel=preload; as=image', false);
header('Link: </style.css>; rel=preload; as=style', false);
// ... generate response ...
echo $html;
```

## Logging & Debugging

Write access logs to a file in JSON format.

```bash
example.com {
    log {
        output file /var/log/frankenphp/access.log
        format json
    }
    php_server
}
```

Enable verbose debug logging in the global options block.

```bash
{
    debug
}
example.com {
    php_server
}
```

`journalctl -u frankenphp -f` — Follow FrankenPHP logs in real-time via the systemd journal.

```bash
journalctl -u frankenphp -f
```

`curl -s http://localhost:2019/config/ | jq '.'` — Inspect the current FrankenPHP/Caddy configuration via the admin API.

```bash
curl -s http://localhost:2019/config/ | jq '.'
```

`curl -s http://localhost:2019/reverse_proxy/upstreams` — Check upstream health status via the admin API.

```bash
curl -s http://localhost:2019/reverse_proxy/upstreams | jq '.'
```

## Practical Examples

Production-ready PHP site with compression and hardened security headers.

```bash
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 worker-mode deployment with auto-sized worker pool and compression.

```bash
example.com {
    root * /var/www/app/public
    php_server {
        worker {
            file /var/www/app/public/worker.php
            num auto
        }
    }
    encode gzip zstd
}
```

Redirect www to apex domain and serve the PHP app.

```bash
www.example.com {
    redir https://example.com{uri} permanent
}
example.com {
    root * /var/www/app/public
    php_server
}
```

Hardened production setup: admin API disabled, ACME email set, logging to file.

```bash
{
    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` — Quickly serve the current directory as a PHP app on port 8080 without TLS.

```bash
docker run --rm -p 8080:8080 -e SERVER_NAME=:8080 -v $PWD:/app/public dunglas/frankenphp
```

<!-- PROSE:outro -->
## Conclusion

FrankenPHP collapses the classic web-server-plus-PHP-FPM stack into one self-contained binary and, with worker mode, turns PHP into a high-throughput application server that rivals Node.js or Go for request latency. The trade-off is statefulness: because your code stays resident between requests, you have to watch for memory leaks and state that bleeds from one request into the next — keep request handlers clean and reset global state inside the loop. As with Caddy, HTTPS is fully automatic, so a production deployment is often just a domain name and a `php_server` directive away.

## Further Reading

- [FrankenPHP – official documentation](https://frankenphp.dev/docs/) – installation, worker mode and configuration reference
- [FrankenPHP on GitHub](https://github.com/php/frankenphp) – source code, releases and issue tracker
<!-- PROSE:outro:end -->

## Related Commands

- [apache](https://www.jpkc.com/db/en/cheatsheets/web-servers/apache/) – the established web server FrankenPHP can replace
- [caddy](https://www.jpkc.com/db/en/cheatsheets/web-servers/caddy/) – the web server FrankenPHP is built on
- [certbot](https://www.jpkc.com/db/en/cheatsheets/web-servers/certbot/) – obtain TLS certificates manually (FrankenPHP does this automatically)

