A Modern PHP App Server

The Author

The current state

A Webserver (Apache, NGINX, …) using PHP via mod_php or php-fpm.

With FrankenPHP

PHP and a Webserver in a single binary

But how?

  • FrankenPHP is a module on top of Caddy
  • Uses its own SAPI (Server Application Programming Interface)
  • The Static Build uses static-php - https://static-php.dev/

Features at a glance

  • Worker mode
  • 103 Early Hints
  • Real-time - MercureHub
  • Prometheus metrics
  • Native support for HTTPS, HTTP/2 and HTTP/3.
  • HTTPS Automation
  • Graceful reload
  • Create a self-contained binary

Basic configuration

Caddyfile:

{
    # Enable FrankenPHP
    frankenphp
    order php_server before file_server
}

localhost {
    # Enable compression (optional)
    encode zstd br gzip
    # Execute PHP files in the current directory and serve assets
    php_server
}

Worker mode

  • Boot the application once and keep it in memory.
  • Requires a "APP_RUNTIME" which is the entrypoint of your application. (For TYPO3 we need a customized ''index.php'')

Configure worker mode:

In this example FrankenPHP starts 2 workers per CPU

{
  # ...
  frankenphp {
    worker ./public/index.php 2
  }
  # ...
}

Early Hints 103

Allow the browser to download resources or preconnect to a site before the final response was sent.

header('Link: </style.css>; rel=preload; as=style');
headers_send(103);

// Do sluggish stuff 🐌

Real-Time - MercureHub

MercureHub config

  • Enable MercureHub
  • Install the "symfony/mercure" package
  • The Mercure Protocol is in Draft: https://www.ietf.org/archive/id/draft-dunglas-mercure-07.html



Caddyfile example configuration:

localhost {
	# ...
	# Enable Mercure hub
	mercure {
		# Transport to use (default to Bolt)
		transport_url {$MERCURE_TRANSPORT_URL:bolt:///data/mercure.db}
		# Publisher JWT key
		publisher_jwt {env.MERCURE_PUBLISHER_JWT_KEY} {env.MERCURE_PUBLISHER_JWT_ALG}
		# Subscriber JWT key
		subscriber_jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {env.MERCURE_SUBSCRIBER_JWT_ALG}
		# Allow anonymous subscribers (double-check that it's what you want)
		anonymous
		# Enable the subscription API (double-check that it's what you want)
		subscriptions
		# Extra directives
		{$MERCURE_EXTRA_DIRECTIVES}

        	# Development only, enables the mercure UI
		# demo
	}
}

Real-Time example

Publish a message to all clients

$hubUrl = 'https://localhost:8788/.well-known/mercure';
$jwt = '<JWT>';

$defaults = HttpClientInterface::OPTIONS_DEFAULTS;
$client = HttpClient::create($defaults);
$hub = new Hub($hubUrl, new StaticTokenProvider($jwt), null, null, $client);

// The topic you want to publish to
$topic = '/chat/messages';
$update = new Update($topic, 'MESSAGE TO ALL CLIENTS');
$hub->publish($update);

Receive the message:

let endpoint = "https://localhost:8788/.well-known/mercure"
let topic = "/chat/messages";
let jwtToken = "<JWT>"

let eventSource = new EventSource(`${endpoint}?topic=${encodeURIComponent(topic)}`, {
  headers: {
    Authorization: `Bearer ${jwtToken}`,
  },
});

let eventSource.onmessage = (event) => {
  console.log("Received event:", event.data);
};

Prometheus metrics

  • The Prometheus exporter is integrated by default (must be enabled)
  • Requires a Prometheus instance (scrapes and stores the metrics)
  • Most likely to be used with Grafana

Enable it in the Caddyfile:

{
    # ...
    servers {
        metrics
    }
    # ...
}

The exporter is available under http://localhost:2019/metrics

Example metric:

# HELP caddy_http_request_duration_seconds Histogram of round-trip request durations.
# TYPE caddy_http_request_duration_seconds histogram
caddy_http_request_duration_seconds_bucket{code="200",handler="subroute",method="GET",server="srv0",le="0.005"} 564
...

Prometheus Server

Scrape the metrics provided by FrankenPHP:

prometheus.yml:

scrape_configs:
  - job_name: 'franky'
    metrics_path: '/metrics'
    static_configs:
      - targets: [ "localhost:2019" ]

Graceful Reload

Restart without interrupting current users connection.

frankenphp reload -c Caddyfile

Create a self-contained binary

Wrap your PHP application into a self-contained binary.

static-build.Dockerfile
FROM --platform=linux/amd64 dunglas/frankenphp:static-builder

# Copy your app
WORKDIR /go/src/app/dist/app
COPY . .

# Build the static binary
WORKDIR /go/src/app/
RUN EMBED=dist/app/ ./build-static.sh

Create the self-contained binary using Docker:

docker build -t static-app -f static-build.Dockerfile . --load

Extract the binary (frankenphp-mac-arm64)

docker cp $(docker create --name static-app-tmp static-app):/go/src/app/dist/frankenphp-linux-x86_64 my-app ; docker rm static-app-tmp

Run FrankenPHP

Docker:

docker run -v $PWD:/app/public \
  -p 80:80 -p 443:443 -p 443:443/udp \
  dunglas/frankenphp

Standalone Binary

./frankenphp run -c Caddyfile

Standalone Binary - CLI

Does not require additional PHP to be installed!

./frankenphp php-cli /path/to/your/script.php

TYPO3 integration

  • MercureHub
  • Prometheus
  • WorkerMode (works, but has issues)

Feature idea:

Using GoRoutines to run e.g. the TYPO3 MessageBus (symfony/messenger) and the Scheduler

Let's have a look

Join the Chat

Demo: https://franky.knallimall.org/

Code: https://gitlab.knallimall.org/ochorocho/franky

Docs: https://frankenphp.dev/docs/