Skip to content

Commit f95795f

Browse files
committed
Add Mercure support to FrankenPHP with environment variables and update documentation
1 parent c02169d commit f95795f

8 files changed

Lines changed: 81 additions & 4 deletions

File tree

‎.gitignore‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ yarn.lock
55
node_modules
66
php-versions.yml
77
*.tmp
8-
/docs/_OLD_
8+
/docs/_OLD_
9+
.playwright-mcp

‎docs/content/docs/2.image-variations/frankenphp.md‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,49 @@ Laravel Octane only relays FrankenPHP's `stderr` and only understands JSON, so l
352352

353353
Control the verbosity with `LOG_OUTPUT_LEVEL`. It defaults to `info` for FrankenPHP so request logs are included. Set it to `warn` to log problems only.
354354

355+
## Mercure
356+
[Mercure](https://mercure.rocks){target="_blank"} pushes real-time updates from your app to the browser. FrankenPHP has a Mercure hub built in, and you turn it on with environment variables instead of editing a Caddyfile. The hub answers at `/.well-known/mercure` on the same ports as your app, in classic mode and with Laravel Octane.
357+
358+
```yml [compose.yml]
359+
services:
360+
php:
361+
image: serversideup/php:8.5-frankenphp
362+
ports:
363+
- "80:8080"
364+
volumes:
365+
- ./:/var/www/html
366+
environment:
367+
MERCURE_ENABLED: "true"
368+
MERCURE_TRUSTED_ISSUERS: "https://example.com"
369+
MERCURE_PUBLISHER_JWT_KEY: "${MERCURE_JWT_SECRET}"
370+
MERCURE_SUBSCRIBER_JWT_KEY: "${MERCURE_JWT_SECRET}"
371+
```
372+
373+
Docker Compose reads `MERCURE_JWT_SECRET` from your `.env` file. Generate a secret with `openssl rand -base64 32`.
374+
375+
| Variable | Default | Description |
376+
|----------|---------|-------------|
377+
| `MERCURE_ENABLED` | `false` | Set to `true` to turn on the Mercure hub |
378+
| `MERCURE_TRUSTED_ISSUERS` | `https://localhost` | The `iss` claim your tokens carry, usually your app's URL. The hub rejects tokens from any other issuer |
379+
| `MERCURE_PUBLISHER_JWT_KEY` | | Shared secret or PEM public key that verifies publisher tokens. Required when the hub is on |
380+
| `MERCURE_PUBLISHER_JWT_ALG` | `HS256` | Algorithm for the publisher key. A PEM key needs an asymmetric algorithm such as `RS256` |
381+
| `MERCURE_SUBSCRIBER_JWT_KEY` | | Shared secret or PEM public key that verifies subscriber tokens. Required when the hub is on |
382+
| `MERCURE_SUBSCRIBER_JWT_ALG` | `HS256` | Algorithm for the subscriber key |
383+
| `MERCURE_EXTRA_DIRECTIVES` | `""` | More [Mercure directives](https://mercure.rocks/docs/deployment/configuration){target="_blank"}, one per line |
384+
385+
The hub only accepts subscribers with a valid token. Use `MERCURE_EXTRA_DIRECTIVES` to allow anonymous subscribers to public updates or to set CORS origins:
386+
387+
```yml [compose.yml]
388+
environment:
389+
MERCURE_EXTRA_DIRECTIVES: |
390+
anonymous
391+
cors_origins https://example.com
392+
```
393+
394+
::note
395+
FrankenPHP 1.13 includes Mercure 1.0, which expects [OAuth 2.0 access tokens](https://github.com/dunglas/mercure/blob/v1.0.3/docs/UPGRADE.md#migrate-your-tokens){target="_blank"} with `iss`, `aud`, and `exp` claims. If your app or library still signs Mercure 0.x tokens, set `MERCURE_EXTRA_DIRECTIVES: "protocol_version_compatibility 8"` while you migrate. Compatibility mode relaxes token checks, so remove it once your tokens are updated.
396+
::
397+
355398
## Environment Variables
356399
The FrankenPHP variation supports extensive customization through environment variables.
357400

‎docs/content/docs/3.framework-guides/1.laravel/octane.md‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ Octane passes its command line options to FrankenPHP through environment variabl
190190
The `max_execution_time` setting in `config/octane.php` works as [documented by Laravel](https://laravel.com/docs/13.x/octane#specifying-the-max-execution-time){target="_blank"}, because Octane passes it to the worker script rather than to the Caddyfile.
191191

192192
Octane also sets `CADDY_GLOBAL_OPTIONS` and `CADDY_SERVER_EXTRA_DIRECTIVES` for its own use, so any value you set for those variables is replaced when Octane starts FrankenPHP:
193-
- `CADDY_SERVER_EXTRA_DIRECTIVES` carries the Mercure settings from `config/octane.php`, so Mercure works as [documented by FrankenPHP](https://frankenphp.dev/docs/laravel/#mercure-support){target="_blank"}.
193+
- `CADDY_SERVER_EXTRA_DIRECTIVES` carries the `mercure` array from `config/octane.php`. Use the `MERCURE_*` variables instead. See [Mercure](#mercure).
194194
- `CADDY_GLOBAL_OPTIONS` is not applied in Octane mode, because Octane sets it to `auto_https disable_redirects`, which would conflict with `CADDY_AUTO_HTTPS`. If you need additional global options, mount a `.caddyfile` into `/etc/frankenphp/caddyfile-global.d/`.
195195

196196
Octane's own Caddyfile asks for JSON logs through `CADDY_SERVER_LOGGER`. Our Caddyfile does not read that variable, because Caddy already writes JSON when Octane starts it. See [Logging](#logging).
@@ -211,6 +211,11 @@ See the [Octane 2.14.0 release notes](https://github.com/laravel/octane/releases
211211

212212
Octane sets `APP_PUBLIC_PATH` to your application's public directory, but our Caddyfile looks for `frankenphp-worker.php` in `CADDY_SERVER_ROOT` instead. This keeps the worker script and the document root in the same place. If you changed `APP_BASE_DIR`, set `CADDY_SERVER_ROOT` to match, just like classic mode.
213213

214+
## Mercure
215+
Turn on FrankenPHP's Mercure hub with the `MERCURE_*` environment variables, the same as classic mode, and leave the `mercure` array out of `config/octane.php`. [Read how to set up Mercure →](/docs/image-variations/frankenphp#mercure)
216+
217+
[FrankenPHP's Laravel docs](https://frankenphp.dev/docs/laravel/#mercure-support){target="_blank"} configure the hub through that array instead. Octane passes it to our Caddyfile in `CADDY_SERVER_EXTRA_DIRECTIVES`, which lands in every site block. With `SSL_MODE=mixed` or `full` your app is served from more than one site block, so the array creates more than one hub, and Mercure 1.0 refuses to start with more than one unnamed hub. The environment variables name the hub, so every site shares it.
218+
214219
## Running Octane Without FrankenPHP
215220

216221
Swoole, Open Swoole, and RoadRunner all run from the [`cli`](/docs/image-variations/cli) image. It already ships what Octane needs on the PHP side: the `pcntl` extension, Composer, `install-php-extensions`, our entrypoint scripts (including the [Laravel automations](/docs/framework-guides/laravel/automations)), and an unprivileged `www-data` user.

‎docs/content/docs/5.guide/5.major-version-migrations.md‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ Octane also needs the Caddy admin API and sets `CADDY_GLOBAL_OPTIONS` for itself
141141
Version 4 set `$_SERVER['REMOTE_ADDR']` to the TCP peer on FrankenPHP, even when Caddy had already worked out the real client IP for the access log. It now matches what Caddy resolved, the same as NGINX and Apache, and Caddy runs in strict mode so a client behind a trusted proxy cannot forge it. You are affected if anything in your app compares `REMOTE_ADDR` to a proxy's address. [Read the trusted proxies guide →](/docs/guide/configuring-trusted-proxies)
142142

143143
#### FrankenPHP: Mercure 1.0 rejects `publisher_jwt` and `subscriber_jwt`
144-
Version 5 ships FrankenPHP 1.13, which includes Mercure 1.0. If you enable the Mercure hub with `publisher_jwt` or `subscriber_jwt`, including through the `mercure` array in `config/octane.php`, FrankenPHP now fails to start. Move the keys into an `issuer` block as shown in [FrankenPHP's Laravel guide](https://frankenphp.dev/docs/laravel/#mercure-support){target="_blank"}, or add `protocol_version_compatibility 8` to keep your current settings while you migrate. The [Mercure 1.0 upgrade guide](https://github.com/dunglas/mercure/blob/v1.0.3/docs/UPGRADE.md){target="_blank"} covers the client changes.
144+
Version 5 ships FrankenPHP 1.13, which includes Mercure 1.0. If you enable the Mercure hub with `publisher_jwt` or `subscriber_jwt`, including through the `mercure` array in `config/octane.php`, FrankenPHP now fails to start. Turn the hub on with the new `MERCURE_*` environment variables instead, which work in classic mode and with Octane. If your app still signs 0.x tokens, add `protocol_version_compatibility 8` to `MERCURE_EXTRA_DIRECTIVES` while you migrate. [Read how to set up Mercure →](/docs/image-variations/frankenphp#mercure) The [Mercure 1.0 upgrade guide](https://github.com/dunglas/mercure/blob/v1.0.3/docs/UPGRADE.md){target="_blank"} covers the client changes.
145145

146146
#### FrankenPHP: Caddy limits request headers
147147
FrankenPHP 1.13 includes [Caddy 2.11.7](https://github.com/caddyserver/caddy/releases/tag/v2.11.7){target="_blank"}. Requests with more than 16 KiB of headers now get a `431 Request Header Fields Too Large` response, where Version 4 allowed 1 MB. Large cookies are the usual cause. Headers with a `.` in their name are now dropped, like headers with a `_`, because PHP reads both as `-` and a client could use them to spoof headers like `X-Forwarded-For`.
@@ -182,6 +182,7 @@ The OPcache values apply only when `PHP_OPCACHE_ENABLE=1`, and the memory is onl
182182
- `TRUSTED_PROXY` - Which proxy IPs to trust for the real client IP: `cloudflare` (default), `sucuri`, `local`, or `off`. Works on `fpm-nginx`, `fpm-apache`, and `frankenphp`.
183183
- `CADDY_ACME_PROFILE` - Select a Let's Encrypt certificate profile on FrankenPHP: `shortlived` (required for IP-address certificates), `tlsserver`, `classic`, or `off` (default).
184184
- `LARAVEL_OCTANE` - Set by Octane, not by you. The FrankenPHP Caddyfile uses it to switch into worker mode.
185+
- `MERCURE_ENABLED`, `MERCURE_TRUSTED_ISSUERS`, `MERCURE_PUBLISHER_JWT_KEY`, `MERCURE_SUBSCRIBER_JWT_KEY`, and `MERCURE_EXTRA_DIRECTIVES` - Run FrankenPHP's Mercure hub. See the breaking change above.
185186
- `AUTORUN_LARAVEL_SKIP_IF_NOT_FOUND` - Lets the container start when Laravel is not in `APP_BASE_DIR` yet, for example before the first `composer install`. Defaults to `false`.
186187

187188
[See the full list of environment variables →](/docs/reference/environment-variable-specification)
@@ -191,6 +192,7 @@ The OPcache values apply only when `PHP_OPCACHE_ENABLE=1`, and the memory is onl
191192
- **Trusted proxies on every web server** - `TRUSTED_PROXY` gives `fpm-nginx`, `fpm-apache`, and `frankenphp` the same Cloudflare, Sucuri, local, or off behavior, and all three resolve the client IP through more than one Docker hop. [Read the trusted proxies guide →](/docs/guide/configuring-trusted-proxies)
192193
- **Short-lived and IP-address certificates** - FrankenPHP can request Let's Encrypt's short-lived profile with `CADDY_ACME_PROFILE`. [Read about short-lived certificates →](/docs/deployment-and-production/configuring-ssl#short-lived--ip-address-certificates)
193194
- **Laravel Nightwatch health check** - `healthcheck-nightwatch` runs `php artisan nightwatch:status` so Docker can watch the agent. [Read the Nightwatch guide →](/docs/framework-guides/laravel/nightwatch)
195+
- **Mercure from environment variables** - Set `MERCURE_ENABLED=true` and your JWT keys to run FrankenPHP's Mercure hub, in classic mode or with Octane. [Read how to set up Mercure →](/docs/image-variations/frankenphp#mercure)
194196
- **FrankenPHP redacts the `authorization` query parameter** - Request logs never contain the JWT that Mercure 0.x subscribers pass in the URL, in every log format. [Read about FrankenPHP logging →](/docs/image-variations/frankenphp#logging)
195197
- **Every image is tested before it is published** - Each image is started on `amd64` and `arm64` and checked before it reaches Docker Hub. If one image fails, nothing from that build is published. [Read what happens when you open a pull request →](/docs/getting-started/contributing#what-happens-when-you-open-a-pull-request)
196198

@@ -204,7 +206,7 @@ The OPcache values apply only when `PHP_OPCACHE_ENABLE=1`, and the memory is onl
204206
- If your app calls `session_start()` itself and reads the session cookie from JavaScript, add `PHP_SESSION_COOKIE_HTTPONLY=Off`
205207
- If you run FrankenPHP and something reads only `stdout`, add `CADDY_LOG_OUTPUT=stdout`. If something parses the `console` lines, or you prefer them when reading logs by eye, add `CADDY_LOG_FORMAT=console`. Skip both if you run Laravel Octane
206208
- If you run Laravel Octane, add `--caddyfile=/etc/frankenphp/Caddyfile` to your `octane:start` command and remove any `FRANKENPHP_CONFIG` worker block or `CADDY_PHP_SERVER_OPTIONS` you added to make Octane work
207-
- If you run the Mercure hub on FrankenPHP, move `publisher_jwt` and `subscriber_jwt` into an `issuer` block, or add `protocol_version_compatibility 8`
209+
- If you run the Mercure hub on FrankenPHP, set `MERCURE_ENABLED=true` and your keys with the `MERCURE_*` variables, and remove the `mercure` array from `config/octane.php`
208210

209211
#### Dockerfile
210212
- If you append to `/etc/s6-overlay/s6-rc.d/<service>/dependencies` for `php-fpm`, `nginx`, or `apache2`, move each line to an empty file in that service's `dependencies.d/` directory

‎docs/content/docs/8.reference/1.environment-variable-specification.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ Setting environment variables all depends on what method you're using to run you
6969
`HEALTHCHECK_SSL_PRIVATE_KEY_FILE`<br />*Default: "/etc/ssl/healthcheck/localhost.key"*|Set the path to the SSL private key for the health check endpoint.| fpm-apache, fpm-nginx, frankenphp
7070
`LARAVEL_OCTANE`<br />*Default: unset*<br />*Set by Laravel Octane*|ℹ️ You do not set this variable. Laravel Octane passes `LARAVEL_OCTANE=1` to FrankenPHP when `octane:start` launches it, and `/etc/frankenphp/Caddyfile` uses it to detect Octane. When it is present, the Caddyfile loads `frankenphp-worker.php` from `CADDY_SERVER_ROOT` as a worker, routes requests to it instead of `index.php`, and enables the Caddy admin API on `localhost` (port `2019` unless Octane passes another). ⚠️ If it is set outside of Octane, the value must be exactly `1`. The Caddyfile imports a file named after the value, so `true`, `on`, or an empty string stops FrankenPHP from starting with a "File to import not found" error. See [Laravel Octane](/docs/framework-guides/laravel/octane).|frankenphp
7171
`LOG_OUTPUT_LEVEL`<br />*Default:* <br /> *"warn" (for all)* <br /> *"info" (for frankenphp)*|Set the verbosity level for container output and service logs. Valid values (least to most verbose): `emerg`, `alert`, `crit`, `error`, `warn`, `notice`, `info`, `debug`. Each level is translated to the native log configuration for PHP, PHP-FPM, and the active web server. <br />ℹ️ FrankenPHP defaults to `info` because Caddy unifies access and error logs — setting `warn` would suppress HTTP request logs entirely (unlike Apache/NGINX where access logs are a separate directive).|all
72+
`MERCURE_ENABLED`<br />*Default: "false"*|Set to `true` to turn on FrankenPHP's Mercure hub at `/.well-known/mercure`. Requires `MERCURE_PUBLISHER_JWT_KEY` and `MERCURE_SUBSCRIBER_JWT_KEY`. See [Mercure](/docs/image-variations/frankenphp#mercure).|frankenphp
73+
`MERCURE_EXTRA_DIRECTIVES`<br />*Default: ""*|Add directives to the Mercure hub, one per line, such as `anonymous` or `cors_origins`. (<a target="_blank" href="https://mercure.rocks/docs/deployment/configuration">Official docs</a>)|frankenphp
74+
`MERCURE_PUBLISHER_JWT_ALG`<br />*Default: "HS256"*|Set the algorithm for the publisher key. A PEM key needs an asymmetric algorithm such as `RS256`.|frankenphp
75+
`MERCURE_PUBLISHER_JWT_KEY`<br />*Default: unset*|Set the shared secret or PEM public key that verifies publisher tokens.|frankenphp
76+
`MERCURE_SUBSCRIBER_JWT_ALG`<br />*Default: "HS256"*|Set the algorithm for the subscriber key.|frankenphp
77+
`MERCURE_SUBSCRIBER_JWT_KEY`<br />*Default: unset*|Set the shared secret or PEM public key that verifies subscriber tokens.|frankenphp
78+
`MERCURE_TRUSTED_ISSUERS`<br />*Default: "https://localhost"*|Set the `iss` claim your Mercure tokens carry, usually your app's URL. The hub rejects tokens from any other issuer.|frankenphp
7279
`NGINX_ACCESS_LOG`<br />*Default: "/dev/stdout"*|Set the default output stream for access log.|fpm-nginx
7380
`NGINX_ERROR_LOG`<br />*Default: "/dev/stderr"*|Set the default output stream for error log.|fpm-nginx
7481
`NGINX_FASTCGI_BUFFERS`<br />*Default: "8 8k"*|Sets the number and size of the buffers used for reading a response from a FastCGI server. (<a target="_blank" href="https://nginx.org/en/docs/http/ngx_http_fastcgi_module.html#fastcgi_buffers">Official Docs</a>)|fpm-nginx

‎src/variations/frankenphp/etc/frankenphp/Caddyfile‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@
6767
@indexphp path_regexp indexphp ^/index\.php(/.+)$
6868
redir @indexphp {re.indexphp.1} 301
6969

70+
import mercure/{$MERCURE_ENABLED:false}.caddyfile
71+
7072
php_server {
7173
# FrankenPHP sets REMOTE_ADDR from the TCP peer, which ignores trusted_proxies
7274
env REMOTE_ADDR {client_ip}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# The Mercure hub is off. Set MERCURE_ENABLED=true to turn it on.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# SSL_MODE=mixed and full serve the app from more than one site, and Mercure
2+
# refuses to start with more than one unnamed hub. Naming the hub "default",
3+
# the name Mercure gives an unnamed hub, lets every site share it while health
4+
# checks and metrics stay the same as a stock Mercure setup.
5+
mercure {
6+
name default
7+
issuer {$MERCURE_TRUSTED_ISSUERS:https://localhost} {
8+
publisher {
9+
jwt {env.MERCURE_PUBLISHER_JWT_KEY} {$MERCURE_PUBLISHER_JWT_ALG:HS256}
10+
}
11+
subscriber {
12+
jwt {env.MERCURE_SUBSCRIBER_JWT_KEY} {$MERCURE_SUBSCRIBER_JWT_ALG:HS256}
13+
}
14+
}
15+
{$MERCURE_EXTRA_DIRECTIVES}
16+
}

0 commit comments

Comments
 (0)