From 0dd5afcafb11d0b8c5d4e8ad6f970271812362e2 Mon Sep 17 00:00:00 2001 From: Rob Lyons <39706150+aSeriousDeveloper@users.noreply.github.com> Date: Wed, 12 Nov 2025 11:22:48 +0000 Subject: [PATCH 1/3] Provide further Octane support for Caddyfile Specify the `CADDY_SERVER_WORKER_DIRECTIVE` and `CADDY_SERVER_WATCH_DIRECTIVES` directives that are populated by Laravel Octane --- src/variations/frankenphp/etc/frankenphp/Caddyfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/variations/frankenphp/etc/frankenphp/Caddyfile b/src/variations/frankenphp/etc/frankenphp/Caddyfile index 566911126..a6eb6f757 100644 --- a/src/variations/frankenphp/etc/frankenphp/Caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/Caddyfile @@ -16,6 +16,8 @@ frankenphp { {$FRANKENPHP_CONFIG} + {$CADDY_SERVER_WORKER_DIRECTIVE} + {$CADDY_SERVER_WATCH_DIRECTIVES} } servers { From 810b6377f6854954d14d8ace4536f155fae448ac Mon Sep 17 00:00:00 2001 From: Jay Rogers Date: Wed, 16 Sep 2026 14:39:54 +0000 Subject: [PATCH 2/3] Enhance FrankenPHP integration with Laravel Octane - Updated environment variable documentation to clarify the behavior of CADDY_ADMIN, CADDY_GLOBAL_OPTIONS, CADDY_LOG_FORMAT, and CADDY_LOG_OUTPUT when using Laravel Octane. - Added a new health check command for Octane to verify server status. - Refactored the test image script to handle multiple containers and improve health check logic. - Modified SSL generation script to account for FrankenPHP's unique requirements. - Adjusted Dockerfile to set default logging format and output for better compatibility with Octane. - Enhanced Caddyfile configuration to support Octane's worker and global options. - Introduced new log format files to handle authorization redaction in logs. - Created README files in configuration directories to guide users on custom Caddyfile usage. - Implemented separate Caddyfile configurations for Octane to manage worker and global settings effectively. --- .../6.default-configurations.md | 22 +++ .../docs/2.image-variations/frankenphp.md | 25 ++- .../3.framework-guides/1.laravel/octane.md | 132 +++++++++++--- .../5.guide/5.major-version-migrations.md | 31 +++- .../1.environment-variable-specification.md | 13 +- .../docs/8.reference/2.command-reference.md | 5 + scripts/test-image.sh | 165 ++++++++++++++---- .../etc/entrypoint.d/5-generate-ssl.sh | 9 +- src/variations/frankenphp/Dockerfile | 7 +- .../frankenphp/etc/frankenphp/Caddyfile | 10 +- .../caddyfile-global.d/README.caddyfile | 7 + .../frankenphp/caddyfile.d/README.caddyfile | 7 + .../laravel-octane/frankenphp/0.caddyfile | 1 + .../laravel-octane/frankenphp/1.caddyfile | 9 + .../laravel-octane/global/0.caddyfile | 3 + .../laravel-octane/global/1.caddyfile | 7 + .../laravel-octane/php-server/0.caddyfile | 1 + .../laravel-octane/php-server/1.caddyfile | 4 + .../etc/frankenphp/log-format/auto.caddyfile | 9 + .../frankenphp/log-format/console.caddyfile | 7 + .../etc/frankenphp/log-format/json.caddyfile | 7 + .../log-level/address/alert.caddyfile | 4 +- .../log-level/address/crit.caddyfile | 4 +- .../log-level/address/debug.caddyfile | 4 +- .../log-level/address/emerg.caddyfile | 4 +- .../log-level/address/error.caddyfile | 4 +- .../log-level/address/info.caddyfile | 4 +- .../log-level/address/notice.caddyfile | 4 +- .../log-level/address/warn.caddyfile | 4 +- .../log-level/global/alert.caddyfile | 4 +- .../log-level/global/crit.caddyfile | 4 +- .../log-level/global/debug.caddyfile | 4 +- .../log-level/global/emerg.caddyfile | 4 +- .../log-level/global/error.caddyfile | 4 +- .../log-level/global/info.caddyfile | 4 +- .../log-level/global/notice.caddyfile | 4 +- .../log-level/global/warn.caddyfile | 4 +- 37 files changed, 437 insertions(+), 108 deletions(-) create mode 100644 src/variations/frankenphp/etc/frankenphp/caddyfile-global.d/README.caddyfile create mode 100644 src/variations/frankenphp/etc/frankenphp/caddyfile.d/README.caddyfile create mode 100644 src/variations/frankenphp/etc/frankenphp/laravel-octane/frankenphp/0.caddyfile create mode 100644 src/variations/frankenphp/etc/frankenphp/laravel-octane/frankenphp/1.caddyfile create mode 100644 src/variations/frankenphp/etc/frankenphp/laravel-octane/global/0.caddyfile create mode 100644 src/variations/frankenphp/etc/frankenphp/laravel-octane/global/1.caddyfile create mode 100644 src/variations/frankenphp/etc/frankenphp/laravel-octane/php-server/0.caddyfile create mode 100644 src/variations/frankenphp/etc/frankenphp/laravel-octane/php-server/1.caddyfile create mode 100644 src/variations/frankenphp/etc/frankenphp/log-format/auto.caddyfile create mode 100644 src/variations/frankenphp/etc/frankenphp/log-format/console.caddyfile create mode 100644 src/variations/frankenphp/etc/frankenphp/log-format/json.caddyfile diff --git a/docs/content/docs/1.getting-started/6.default-configurations.md b/docs/content/docs/1.getting-started/6.default-configurations.md index 5541c2aee..e9509f253 100644 --- a/docs/content/docs/1.getting-started/6.default-configurations.md +++ b/docs/content/docs/1.getting-started/6.default-configurations.md @@ -134,6 +134,28 @@ The `cli` variation does not have a health check because it doesn't really make :u-button{to="/docs/guide/using-healthchecks-with-laravel" label="Learn more about health checks" aria-label="Learn more about health checks" size="md" color="primary" variant="outline" trailing-icon="i-lucide-arrow-right" class="font-bold ring ring-inset ring-blue-600 text-blue-600 hover:ring-blue-500 hover:text-blue-500"} +## Logging +Every variation writes its logs to the container's standard output streams instead of files. This is the [twelve-factor app](https://12factor.net/logs){target="_blank"} approach: the process writes its event stream and the environment decides where it goes, so `docker logs`, Docker Compose, Kubernetes, and log collectors all pick it up without any extra setup. + +Which stream a log lands on depends on the web server, and in each case we follow the official Docker image for that server: + +| Variation | Access logs | Error logs | Why | +|-----------|-------------|------------|-----| +| `fpm-nginx` | `stdout` | `stderr` | Matches the [official NGINX image](https://github.com/nginxinc/docker-nginx/blob/master/mainline/debian/Dockerfile){target="_blank"}, which links `access.log` to `/dev/stdout` and `error.log` to `/dev/stderr`. | +| `fpm-apache` | `stdout` | `stderr` | Matches the [official httpd image](https://github.com/docker-library/httpd/blob/master/2.4/Dockerfile){target="_blank"}, which sends `CustomLog` to `stdout` and `ErrorLog` to `stderr`. | +| `fpm` | Off | `stderr` | Matches the [official PHP image](https://github.com/docker-library/php/blob/master/8.4/trixie/fpm/Dockerfile){target="_blank"}. PHP-FPM closes `stdout` when it starts ([PHP bug #73886](https://bugs.php.net/bug.php?id=73886){target="_blank"}), so `stderr` is the only option. | +| `frankenphp` | `stderr` | `stderr` | Caddy's [access log](https://caddyserver.com/docs/caddyfile/directives/log){target="_blank"} and its runtime log share the same [structured format](https://caddyserver.com/docs/logging){target="_blank"} with a level on every entry, and [`stderr` is Caddy's default output](https://caddyserver.com/docs/caddyfile/directives/log#output){target="_blank"} for both. The format is Caddy's default too: JSON unless `stderr` is a terminal. | +| PHP errors (all variations) | | `stderr` | `PHP_ERROR_LOG` defaults to `/dev/stderr`. | + +NGINX and Apache keep two logs, so the Unix convention applies: the access log is the server's normal output and goes to `stdout`, while diagnostics go to `stderr`. Caddy has an access log too, but it is a named logger (`http.log.access`) that shares the same structured format and default output as the runtime log, and every entry carries its own level. Splitting the two by stream would not add anything, so we keep Caddy's default. That also keeps FrankenPHP compatible with tools that build on it, like Laravel Octane, which only reads `stderr`. + +Most platforms treat the two streams the same. One exception is Google Kubernetes Engine, which [tags `stdout` as `INFO` and `stderr` as `ERROR`](https://docs.cloud.google.com/kubernetes-engine/docs/concepts/about-logs){target="_blank"} unless the entry is structured JSON with a severity. If your platform does something similar, FrankenPHP's JSON entries already carry a `level` your pipeline can map to a severity, as long as you have not pinned `CADDY_LOG_FORMAT` to `console`. For the other variations, move the stream with `NGINX_ACCESS_LOG`, `NGINX_ERROR_LOG`, or `PHP_ERROR_LOG`. + +Use `LOG_OUTPUT_LEVEL` to control verbosity in every variation. It maps to the native log level of PHP, PHP-FPM, and the web server. + +:u-button{to="/docs/reference/environment-variable-specification" label="See the logging environment variables" aria-label="See the logging environment variables" size="md" color="primary" variant="outline" trailing-icon="i-lucide-arrow-right" class="font-bold ring ring-inset ring-blue-600 text-blue-600 hover:ring-blue-500 hover:text-blue-500"} + + ## Default Entrypoint Scripts We created an entrypoint script that scans the `entrypoint.d` directory for other shell scripts to execute before the main container process starts. All scripts are executed in alphabetical order so you can have full control over what script execution order. diff --git a/docs/content/docs/2.image-variations/frankenphp.md b/docs/content/docs/2.image-variations/frankenphp.md index 936bd7762..49c4fdc11 100644 --- a/docs/content/docs/2.image-variations/frankenphp.md +++ b/docs/content/docs/2.image-variations/frankenphp.md @@ -261,7 +261,7 @@ The FrankenPHP variation uses ports 8080 and 8443 (instead of 80 and 443) to all :: ### Laravel Octane -Laravel Octane natively supports FrankenPHP. Use our guide below to learn more. +Laravel Octane natively supports FrankenPHP. Pass `--caddyfile=/etc/frankenphp/Caddyfile` to `octane:start` and our Caddyfile switches into worker mode while keeping the same production configuration as classic mode. Use our guide below to learn more. :u-button{to="/docs/framework-guides/laravel/octane" label="Learn more about Laravel Octane" aria-label="Learn more about Laravel Octane" size="md" color="primary" variant="outline" trailing-icon="i-lucide-arrow-right" class="font-bold ring ring-inset ring-blue-600 text-blue-600 hover:ring-blue-500 hover:text-blue-500"} @@ -331,6 +331,25 @@ Learn more about SSL modes in the [Configuring SSL](/docs/deployment-and-product :u-button{to="/docs/deployment-and-production/configuring-ssl" label="Learn more about SSL modes" aria-label="Learn more about SSL modes" size="md" color="primary" variant="outline" trailing-icon="i-lucide-arrow-right" class="font-bold ring ring-inset ring-blue-600 text-blue-600 hover:ring-blue-500 hover:text-blue-500"} +## Logging +FrankenPHP is built on Caddy, and Caddy handles logs differently from NGINX and Apache. Caddy does have an [access log](https://caddyserver.com/docs/caddyfile/directives/log){target="_blank"}, but it is a named logger that shares the same [structured format](https://caddyserver.com/docs/logging){target="_blank"} and default output as Caddy's runtime log. Every entry carries its own level: requests are logged at `INFO` and problems at `ERROR`. + +Because both logs share one format and one default output, the FrankenPHP variation sends everything to `stderr`. That is [Caddy's default](https://caddyserver.com/docs/caddyfile/directives/log#output){target="_blank"}, it is what the official FrankenPHP image does, and it is what Laravel Octane expects. Our NGINX and Apache variations keep the traditional split of access logs on `stdout` and error logs on `stderr`, because that is what the official images for those servers do. [Read how we approach logging across all variations →](/docs/getting-started/default-configurations#logging) + +`docker logs`, Docker Compose, and Kubernetes capture both streams, so nothing changes in day-to-day use. + +The format follows Caddy's default as well. Caddy [writes human-readable `console` lines when `stderr` is an interactive terminal and JSON otherwise](https://caddyserver.com/docs/caddyfile/directives/log#format){target="_blank"}. A container started by Docker Compose, Docker Swarm, or Kubernetes has no terminal, so it gets one JSON object per line. That is what log collectors expect, and every entry carries a `level` field your log pipeline can map to a severity instead of guessing from the stream (for example, [GKE tags `stderr` as `ERROR`](https://docs.cloud.google.com/kubernetes-engine/docs/concepts/about-logs){target="_blank"} unless it can read a severity). `docker run -it` and `tty: true` give you the console lines instead. Set `CADDY_LOG_FORMAT` if you want the same format everywhere: +- `CADDY_LOG_FORMAT=console` if you read logs by eye with `docker compose logs` or `docker service logs` and want the colored, human-readable lines whether or not a terminal is attached. +- `CADDY_LOG_FORMAT=json` if a container runs with a terminal attached but you still want structured logs. + +In both formats the request log redacts the `authorization` query parameter, so the JWT that [Mercure subscribers pass in the URL](https://mercure.rocks/spec#authorization){target="_blank"} never lands in your logs. This is the same filter that [FrankenPHP's own Caddyfile](https://github.com/php/frankenphp/blob/main/caddy/frankenphp/Caddyfile){target="_blank"} recommends. + +::warning +Laravel Octane only relays FrankenPHP's `stderr` and only understands JSON, so leave `CADDY_LOG_OUTPUT` and `CADDY_LOG_FORMAT` at their defaults when you run Octane. See [Logging with Octane](/docs/framework-guides/laravel/octane#logging). +:: + +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. + ## Environment Variables The FrankenPHP variation supports extensive customization through environment variables. @@ -345,8 +364,8 @@ The FrankenPHP variation supports extensive customization through environment va | `CADDY_HTTP_PORT` | `8080` | HTTP port | | `CADDY_HTTPS_PORT` | `8443` | HTTPS port | | `CADDY_ADMIN` | `off` | Caddy admin API endpoint | -| `CADDY_LOG_FORMAT` | `console` | Log format (`console`/`json`) | -| `CADDY_LOG_OUTPUT` | `stdout` | Log output destination | +| `CADDY_LOG_FORMAT` | `auto` | Log format: `auto` (Caddy's default, `console` on a terminal and `json` otherwise), `console`, or `json` | +| `CADDY_LOG_OUTPUT` | `stderr` | Log output destination | | `CADDY_GLOBAL_OPTIONS` | `""` | Additional Caddy global options | | `CADDY_SERVER_EXTRA_DIRECTIVES` | `""` | Additional Caddy server directives | | `SSL_MODE` | `off` | SSL mode: `off`, `mixed`, or `full` | diff --git a/docs/content/docs/3.framework-guides/1.laravel/octane.md b/docs/content/docs/3.framework-guides/1.laravel/octane.md index 81c150815..1cf39e93d 100644 --- a/docs/content/docs/3.framework-guides/1.laravel/octane.md +++ b/docs/content/docs/3.framework-guides/1.laravel/octane.md @@ -47,35 +47,34 @@ We'll expand upon this classic mode file and modify it to run Laravel Octane (wh ### Install Laravel Octane -First, install Octane in your Laravel application: +First, install Octane in your Laravel application and tell it to use FrankenPHP. These are the same steps from the [official Laravel documentation](https://laravel.com/docs/13.x/octane#installation){target="_blank"}: ```bash [Terminal] docker compose run php composer require laravel/octane +docker compose run php php artisan octane:install --server=frankenphp ``` -When that command runs, you should see a PHP file that Laravel creates in your `/public` directory. This is required for Laravel Octane to work. +The install command publishes `config/octane.php`, sets `OCTANE_SERVER=frankenphp` in your `.env` file, and creates a PHP file in your `/public` directory. This is the script FrankenPHP keeps running in memory, and Octane needs it to work. ```php [public/frankenphp-worker.php] [ + 'env' => [ + 'CADDY_SERVER_LOG_LEVEL' => 'INFO', + ], +], +``` + +See the [Octane 2.14.0 release notes](https://github.com/laravel/octane/releases/tag/v2.14.0){target="_blank"} for details. +:: + +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. + ## Things to Watch Out For Since Octane is a whole different way of running Laravel compared to traditional PHP-FPM, there are a few things to watch out for. ### Dependency Injection -Be careful with how you inject dependencies into long-lived objects. Injecting the wrong things into constructors can cause requests to "leak" between users. Review Laravel's [Dependency Injection and Octane](https://laravel.com/docs/12.x/octane#dependency-injection-and-octane) documentation for details. +Be careful with how you inject dependencies into long-lived objects. Injecting the wrong things into constructors can cause requests to "leak" between users. Review Laravel's [Dependency Injection and Octane](https://laravel.com/docs/13.x/octane#dependency-injection-and-octane){target="_blank"} documentation for details. ### Memory Leaks -Review Laravel's [Octane documentation on memory leaks](https://laravel.com/docs/12.x/octane#managing-memory-leaks) to understand what to avoid. +Review Laravel's [Octane documentation on memory leaks](https://laravel.com/docs/13.x/octane#managing-memory-leaks){target="_blank"} to understand what to avoid. + +### Reloading After Deployments +Octane keeps your code in memory, so a deployment needs [`php artisan octane:reload`](https://laravel.com/docs/13.x/octane#reloading-the-workers){target="_blank"} to pick up new code. In a container this usually means replacing the container instead. If you do reload in place, our Caddyfile exposes the admin API Octane needs, so `octane:reload` works as expected. ## Learn More - [FrankenPHP Variation Documentation](/docs/image-variations/frankenphp) -- [Laravel Octane Documentation](https://laravel.com/docs/12.x/octane) -- [FrankenPHP Documentation](https://frankenphp.dev/docs/) \ No newline at end of file +- [Laravel Octane Documentation](https://laravel.com/docs/13.x/octane) +- [FrankenPHP Documentation](https://frankenphp.dev/docs/) diff --git a/docs/content/docs/5.guide/5.major-version-migrations.md b/docs/content/docs/5.guide/5.major-version-migrations.md index 3c60b6fb5..b17d2a0c5 100644 --- a/docs/content/docs/5.guide/5.major-version-migrations.md +++ b/docs/content/docs/5.guide/5.major-version-migrations.md @@ -24,7 +24,7 @@ Debian Bullseye and Alpine 3.16 were dropped at the same time. Debian 11 reached If you are on one of these, move to PHP 8.2 or newer on `bookworm`, `trixie`, `alpine3.23`, or `alpine3.24`. See [EOL versions and the legacy-modernization path](https://github.com/serversideup/docker-php/blob/main/SECURITY.md#eol-versions-and-the-legacy-modernization-path). ## Version 4 → Version 5 Migration -Version 5 is about OPcache. Setting `PHP_OPCACHE_ENABLE=1` now gives you tuned defaults instead of PHP's stock values. There is one breaking change, and it only affects you if you run with OPcache enabled while your code is mounted as a volume. +Version 5 is about production polish. Setting `PHP_OPCACHE_ENABLE=1` now gives you tuned defaults instead of PHP's stock values, Laravel Octane runs with our production Caddyfile, and FrankenPHP logs follow Caddy's defaults for stream and format. There are two breaking changes. One affects you if you run with OPcache enabled while your code is mounted as a volume. The other affects you if something in your setup reads FrankenPHP's `stdout` and `stderr` separately, or parses its `console` log lines. If you want to stay on Version 4 while you review the changes, pin your image tag to the last v4 release. Version-pinned tags are never rebuilt, so you will not receive security updates until you move to v5. See [how our releases work](/docs/getting-started/upgrade-guide#how-our-releases-work). @@ -37,9 +37,12 @@ services: ### Why we changed OPcache Most people start with these images in development, so OPcache stays off by default to keep your edits showing up instantly. But when you flip it on for production, the settings behind it should be the ones you would have picked yourself after reading the docs. They were not. Version 4 checked every cached file for changes every two seconds, shipped PHP's stock memory sizes, and documented an environment variable that did nothing. Version 5 fixes all of that with the values from [Symfony's performance guide](https://symfony.com/doc/current/performance.html#configure-opcache-for-maximum-performance){target="_blank"}, which says "The default OPcache configuration is not suited for Symfony applications." [FrankenPHP's performance guide](https://frankenphp.dev/docs/performance/){target="_blank"} points to the same page "even if you don't use Symfony." [Read the production performance tuning guide →](/docs/guide/production-performance-tuning#php-opcache) +### Why we changed FrankenPHP logging +Version 4 sent FrankenPHP's logs to `stdout` to mirror the access log convention of our NGINX and Apache variations. Caddy's access log is a named logger that shares the same [structured format](https://caddyserver.com/docs/logging){target="_blank"} and default output as its runtime log, though, with a level on every entry. Caddy, the official FrankenPHP image, and Laravel Octane all expect that output on `stderr`. Version 4 also forced the human-readable `console` format, which Caddy [only picks by itself when `stderr` is a terminal](https://caddyserver.com/docs/caddyfile/directives/log#format){target="_blank"}. A container has no terminal, so Caddy, the official FrankenPHP image, and Octane all write JSON there, and forcing `console` put ANSI color codes into every log pipeline. Version 5 follows both upstream defaults, which means Octane works without any special handling. [Read how we approach logging →](/docs/getting-started/default-configurations#logging) + ### Breaking changes in Version 5 ::caution -The following change alters behavior for existing configurations that set `PHP_OPCACHE_ENABLE=1`. +The following changes alter behavior for existing configurations that set `PHP_OPCACHE_ENABLE=1`, that consume FrankenPHP's `stdout` and `stderr` separately, or that parse FrankenPHP's `console` log lines. :: #### `PHP_OPCACHE_VALIDATE_TIMESTAMPS` now defaults to `0` @@ -53,6 +56,17 @@ You are affected if you set `PHP_OPCACHE_ENABLE=1` **and** any of these apply: The fix is one of two things: restart the container after code changes (recommended), or set `PHP_OPCACHE_VALIDATE_TIMESTAMPS=1` to restore the Version 4 behavior. +#### `CADDY_LOG_OUTPUT` now defaults to `stderr` and `CADDY_LOG_FORMAT` to `auto` +FrankenPHP logs, including request logs, now go to `stderr` instead of `stdout`, and inside a container they are JSON instead of `console` lines. `docker logs`, Docker Compose, and Kubernetes show both streams, so most setups will only notice the format. + +You are affected if: + +- You separate the streams yourself, for example with `2>/dev/null` or a log driver rule that only captures `stdout` +- You parse the `console` lines, for example with a regular expression in your log pipeline +- You read FrankenPHP logs by eye with `docker compose logs` or `docker service logs` and prefer the colored lines + +The fix is one of two things: keep the defaults and let your log pipeline read the `level` of each JSON entry (recommended), or restore the Version 4 look with `CADDY_LOG_FORMAT=console` and `CADDY_LOG_OUTPUT=stdout`. Do not use either override with Laravel Octane, which only relays `stderr` and only parses JSON. + ### Fixes - `PHP_OPCACHE_FORCE_RESTART_TIMEOUT` existed in Version 4 but never reached `php.ini`. It now works. The default of `180` matches PHP's own default, so nothing changes unless you had set it to something else. @@ -67,6 +81,13 @@ The OPcache values apply only when `PHP_OPCACHE_ENABLE=1`, and the memory is onl | `PHP_OPCACHE_MAX_ACCELERATED_FILES` | `10000` | `32531` | | `PHP_REALPATH_CACHE_TTL` | `120` | `600` | +`CADDY_LOG_OUTPUT` and `CADDY_LOG_FORMAT` apply to the FrankenPHP variation only. See [Why we changed FrankenPHP logging](#why-we-changed-frankenphp-logging). + +| Variable | Version 4 | Version 5 | +|----------|-----------|-----------| +| `CADDY_LOG_OUTPUT` | `stdout` | `stderr` | +| `CADDY_LOG_FORMAT` | `console` | `auto` | + ### New variables - `PHP_OPCACHE_ENABLE_CLI` - Whether CLI commands use OPcache when `PHP_OPCACHE_ENABLE=1`. Defaults to `1`, which is what Version 4 did. Set it to `0` to keep OPcache on for the web server only. - `PHP_OPCACHE_PRELOAD` - Path to a preload script. Symfony generates one for you and [recommends it](https://symfony.com/doc/current/performance.html#use-the-opcache-class-preloading){target="_blank"}. @@ -74,6 +95,10 @@ The OPcache values apply only when `PHP_OPCACHE_ENABLE=1`, and the memory is onl [See the full list of environment variables →](/docs/reference/environment-variable-specification) +### New features in Version 5 +- **Laravel Octane uses our Caddyfile** - Pass `--caddyfile=/etc/frankenphp/Caddyfile` to `octane:start` and the image switches into worker mode with the same trusted proxy support, security headers, asset caching, SSL modes, and health check as classic mode. [Read the Octane guide →](/docs/framework-guides/laravel/octane) +- **FrankenPHP redacts the `authorization` query parameter** - Request logs never contain the JWT that Mercure subscribers pass in the URL, in every log format. [Read about FrankenPHP logging →](/docs/image-variations/frankenphp#logging) + ### V5 Migration Checklist #### Docker Compose @@ -81,6 +106,8 @@ The OPcache values apply only when `PHP_OPCACHE_ENABLE=1`, and the memory is onl - If your code is mounted as a volume with `PHP_OPCACHE_ENABLE=1`, either turn OPcache off for that environment or add `PHP_OPCACHE_VALIDATE_TIMESTAMPS=1` - If you deploy WordPress on a volume, add `PHP_OPCACHE_VALIDATE_TIMESTAMPS=1` or restart the container after updates made outside the WordPress admin - Replace any `docker exec ... artisan optimize` style deployment steps with a container restart +- 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 +- 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 #### Dockerfile - Nothing is required diff --git a/docs/content/docs/8.reference/1.environment-variable-specification.md b/docs/content/docs/8.reference/1.environment-variable-specification.md index 00abcfdf0..bc1579747 100644 --- a/docs/content/docs/8.reference/1.environment-variable-specification.md +++ b/docs/content/docs/8.reference/1.environment-variable-specification.md @@ -46,18 +46,18 @@ Setting environment variables all depends on what method you're using to run you `AUTORUN_LARAVEL_STORAGE_LINK`
*Default: "true"*|Automatically run "php artisan storage:link" on container start.
ℹ️ Requires `AUTORUN_ENABLED = true` to run.| all `AUTORUN_LARAVEL_VIEW_CACHE`
*Default: "true"*|Automatically run "php artisan view:cache" on container start.
ℹ️ Requires `AUTORUN_ENABLED = true` to run.| all `CADDY_ACME_PROFILE`
*Default: "off"*|Select a Let's Encrypt ACME certificate profile. Valid options: `off` (default, no profile — keeps the stock Let's Encrypt + ZeroSSL issuers), `shortlived` (~6-day certs; also required for IP-address certificates), `tlsserver`, or `classic`. Setting any profile pins issuance to Let's Encrypt only and renews more frequently, so the container needs reliable egress to the ACME CA. (Official docs)|frankenphp -`CADDY_ADMIN`
*Default: "off"*|Enable Caddy admin interface. (Official docs)|frankenphp +`CADDY_ADMIN`
*Default: "off"*|Enable Caddy admin interface. ℹ️ Ignored when Laravel Octane starts FrankenPHP. Octane needs the admin API for `octane:status`, `octane:reload`, and `octane:stop`, so the Caddyfile enables it on `localhost` instead. See [Laravel Octane](/docs/framework-guides/laravel/octane#octane-options). (Official docs)|frankenphp `CADDY_AUTO_HTTPS`
*Default: "off"*|Enable automatic HTTPS. (Official docs)|frankenphp -`CADDY_GLOBAL_OPTIONS`
*Default: ""*|Set global options for the Caddy server. (Official docs)|frankenphp +`CADDY_GLOBAL_OPTIONS`
*Default: ""*|Set global options for the Caddy server. ℹ️ Ignored when Laravel Octane starts FrankenPHP, because `octane:start` overwrites this variable with its own value. Mount a `.caddyfile` into `/etc/frankenphp/caddyfile-global.d/` instead. See [Laravel Octane](/docs/framework-guides/laravel/octane#octane-options). (Official docs)|frankenphp `CADDY_HTTP_PORT`
*Default: "8080"*|Set the port for HTTP. (Official docs)|frankenphp `CADDY_HTTP_SERVER_ADDRESS`
*Default: "http://"*|Set the server address for HTTP. (Official docs)|frankenphp `CADDY_HTTPS_PORT`
*Default: "8443"*|Set the port for HTTPS. (Official docs)|frankenphp `CADDY_HTTPS_SERVER_ADDRESS`
*Default: "https://"*|Set the server address for HTTPS. (Official docs)|frankenphp -`CADDY_LOG_FORMAT`
*Default: "console"*|Set the format for the Caddy log. (Official docs)|frankenphp -`CADDY_LOG_OUTPUT`
*Default: "stdout"*|Set the output for the Caddy log. (Official docs)|frankenphp +`CADDY_LOG_FORMAT`
*Default: "auto"*|Set the format for the Caddy log. Valid options: `auto` (Caddy's default: human-readable `console` lines when `stderr` is a terminal and `json` otherwise, so a container gets JSON), `console`, or `json`. Every format redacts the `authorization` query parameter from request logs. ℹ️ Leave this at `auto` when you run Laravel Octane, which only understands JSON. See [Laravel Octane](/docs/framework-guides/laravel/octane#logging). (Official docs)|frankenphp +`CADDY_LOG_OUTPUT`
*Default: "stderr"*|Set the output for the Caddy log. Caddy's access log and runtime log share the same structured format, and `stderr` is Caddy's default output for both. ℹ️ Laravel Octane only relays `stderr`, so leave this at the default when you run Octane. See [how we approach logging](/docs/getting-started/default-configurations#logging). (Official docs)|frankenphp `CADDY_PHP_SERVER_OPTIONS`
*Default: ""*|Set PHP server options for the Caddy server. (Official docs)|frankenphp -`CADDY_SERVER_EXTRA_DIRECTIVES`
*Default: ""*|Set extra directives for the Caddy server. (Official docs)|frankenphp -`CADDY_SERVER_LOG_LEVEL`
*Default: "warn"*|Set the server log level for the Caddy server. This can also be changed with `LOG_OUTPUT_LEVEL`, but `CADDY_SERVER_LOG_LEVEL` takes precedence. (Official docs)|frankenphp +`CADDY_SERVER_EXTRA_DIRECTIVES`
*Default: ""*|Set extra directives for the Caddy server. ℹ️ `octane:start` overwrites this variable with the Mercure settings from `config/octane.php`. See [Laravel Octane](/docs/framework-guides/laravel/octane#octane-options). (Official docs)|frankenphp +`CADDY_SERVER_LOG_LEVEL`
*Default: "warn"*|Set the server log level for the Caddy server. This can also be changed with `LOG_OUTPUT_LEVEL`, but `CADDY_SERVER_LOG_LEVEL` takes precedence. ℹ️ `octane:start` sets this variable itself. Use its `--log-level` option instead. See [Laravel Octane](/docs/framework-guides/laravel/octane#logging). (Official docs)|frankenphp `CADDY_SERVER_ROOT`
*Default: "/var/www/html/public"*|Set the root directory for the Caddy server. (Official docs)|frankenphp `COMPOSER_ALLOW_SUPERUSER`
*Default: "1"*|Disable warning about running as super-user|all `COMPOSER_HOME`
*Default: "/composer"*|The COMPOSER_HOME variable allows you to change the Composer home directory. This is a hidden, global (per-user on the machine) directory that is shared between all projects.|all @@ -67,6 +67,7 @@ Setting environment variables all depends on what method you're using to run you `HEALTHCHECK_PATH`
*Default: "/healthcheck"*|Set the path for the health check endpoint. (Official docs)|all (except `cli` and `frankenphp`) `HEALTHCHECK_SSL_CERTIFICATE_FILE`
*Default: "/etc/ssl/healthcheck/localhost.crt"*|Set the path to the SSL certificate for the health check endpoint.| fpm-apache, fpm-nginx, frankenphp `HEALTHCHECK_SSL_PRIVATE_KEY_FILE`
*Default: "/etc/ssl/healthcheck/localhost.key"*|Set the path to the SSL private key for the health check endpoint.| fpm-apache, fpm-nginx, frankenphp +`LARAVEL_OCTANE`
*Default: unset*
*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 `LOG_OUTPUT_LEVEL`
*Default:*
*"warn" (for all)*
*"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.
ℹ️ 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 `NGINX_ACCESS_LOG`
*Default: "/dev/stdout"*|Set the default output stream for access log.|fpm-nginx `NGINX_ERROR_LOG`
*Default: "/dev/stderr"*|Set the default output stream for error log.|fpm-nginx diff --git a/docs/content/docs/8.reference/2.command-reference.md b/docs/content/docs/8.reference/2.command-reference.md index be62862cc..b1c1660cc 100644 --- a/docs/content/docs/8.reference/2.command-reference.md +++ b/docs/content/docs/8.reference/2.command-reference.md @@ -94,6 +94,11 @@ This command is used to copy our entrypoint scripts into the S6 Overlay scripts ## docker-php-serversideup-s6-install This is a command used at build time to install a specific version of S6 Overlay. +## healthcheck-octane +Runs `php artisan octane:status` so a container health check can confirm that the Octane server is running. It ships in every image, but it only works in a container where Octane started the server. + +[Learn more about Laravel Octane →](/docs/framework-guides/laravel/octane#health-checks) + ## install-php-extensions This command is a wrapper around the `docker-php-ext-install` command that comes with the official PHP Docker images. This command allows you to install PHP extensions with a single command. diff --git a/scripts/test-image.sh b/scripts/test-image.sh index 0aab5bdf0..74d8f5756 100755 --- a/scripts/test-image.sh +++ b/scripts/test-image.sh @@ -101,56 +101,153 @@ if [ -n "$http_port" ]; then run_args+=(--publish "127.0.0.1::${http_port}" --volume "$web_dir:$web_root:ro") fi -container=$(docker run "${run_args[@]}" "$image") +containers=() cleanup() { - docker rm --force "$container" >/dev/null 2>&1 || true + for container in "${containers[@]}"; do + docker rm --force "$container" >/dev/null 2>&1 || true + done [ -z "${web_dir:-}" ] || rm -rf "$web_dir" } trap cleanup EXIT dump_container_state() { echo "--- container logs ---" >&2 - docker logs "$container" >&2 2>&1 || true + docker logs "$1" >&2 2>&1 || true echo "--- last health check ---" >&2 - docker inspect --format '{{range .State.Health.Log}}{{.Output}}{{end}}' "$container" 2>/dev/null | tail -5 >&2 || true + docker inspect --format '{{range .State.Health.Log}}{{.Output}}{{end}}' "$1" 2>/dev/null | tail -5 >&2 || true } -status=starting -for _ in $(seq 1 "$health_timeout_seconds"); do - status=$(docker inspect --format '{{.State.Health.Status}}' "$container" 2>/dev/null || echo "gone") - case "$status" in - healthy) break ;; - unhealthy|gone) break ;; +start_container() { + container=$(docker run "${run_args[@]}" "$@" "$image") + containers+=("$container") + + status=starting + for _ in $(seq 1 "$health_timeout_seconds"); do + status=$(docker inspect --format '{{.State.Health.Status}}' "$container" 2>/dev/null || echo "gone") + case "$status" in + healthy) break ;; + unhealthy|gone) break ;; + esac + sleep 1 + done + + if [ "$status" != "healthy" ]; then + dump_container_state "$container" + fail "Container did not become healthy within ${health_timeout_seconds}s (status: $status)" + fi + [ -z "$http_port" ] || host_port=$(docker port "$container" "$http_port" | head -n1 | sed 's/.*://') +} + +# Retries until the response body matches, since the web server may still be warming up. +expect_body() { + path="$1" + expected="$2" + body="" + for _ in $(seq 1 "$http_timeout_seconds"); do + body=$(curl --silent --show-error --max-time 5 "http://127.0.0.1:${host_port}${path}" 2>/dev/null || true) + [ "$body" = "$expected" ] && return 0 + sleep 1 + done + dump_container_state "$container" + fail "Web server did not serve ${path} on port ${http_port}. Response: ${body:-}" +} + +# Uploaded PHP files under /storage must never run, including through PATH_INFO +# (/storage/file.php/anything), which Apache and FrankenPHP would otherwise execute. +expect_storage_blocked() { + for path in /storage/uploaded.php /storage/uploaded.php/anything; do + response=$(curl --silent --max-time 5 --output /dev/null --write-out '%{http_code}' "http://127.0.0.1:${host_port}${path}" || true) + [ "$response" = "403" ] || fail "Expected ${path} to return 403, got ${response:-}" + done +} + +# Mercure subscribers pass their JWT as ?authorization=, which must never reach the access log. +# Retries because the log line lands a moment after the response. +expect_authorization_redacted() { + curl --silent --max-time 5 --output /dev/null "http://127.0.0.1:${host_port}/?authorization=octane-secret" || true + for _ in $(seq 1 "$http_timeout_seconds"); do + logs=$(docker logs "$container" 2>&1) + case "$logs" in + *"octane-secret"*) fail "Access log contains the authorization query parameter" ;; + *"authorization=REDACTED"*) return 0 ;; + esac + sleep 1 + done + dump_container_state "$container" + fail "Access log does not redact the authorization query parameter" +} + +# A detached container has no terminal, so Caddy's default format is json and its default +# stream is stderr. Octane depends on both: it only relays stderr and only parses JSON. +expect_json_logs_on_stderr() { + stdout_logs=$(docker logs "$container" 2>/dev/null) + stderr_logs=$(docker logs "$container" 2>&1 >/dev/null) + case "$stderr_logs" in + *'"msg":"handled request"'*) ;; + *) dump_container_state "$container"; fail "Access log is not JSON on stderr" ;; esac - sleep 1 -done + case "$stdout_logs" in + *"handled request"*) fail "Access log was written to stdout, which Octane discards" ;; + esac +} -if [ "$status" != "healthy" ]; then - dump_container_state - fail "Container did not become healthy within ${health_timeout_seconds}s (status: $status)" -fi +start_container pass "Container became healthy" [ -n "$http_port" ] || exit 0 -host_port=$(docker port "$container" "$http_port" | head -n1 | sed 's/.*://') -body="" -for _ in $(seq 1 "$http_timeout_seconds"); do - body=$(curl --silent --show-error --max-time 5 "http://127.0.0.1:${host_port}/" 2>/dev/null || true) - [ "$body" = "serversideup-php-ok:${php_version}" ] && break - sleep 1 -done - -if [ "$body" != "serversideup-php-ok:${php_version}" ]; then - dump_container_state - fail "Web server did not serve index.php on port ${http_port}. Response: ${body:-}" -fi +expect_body / "serversideup-php-ok:${php_version}" pass "Web server serves PHP on port ${http_port}" -# Uploaded PHP files under /storage must never run, including through PATH_INFO -# (/storage/file.php/anything), which Apache and FrankenPHP would otherwise execute. -for path in /storage/uploaded.php /storage/uploaded.php/anything; do - response=$(curl --silent --max-time 5 --output /dev/null --write-out '%{http_code}' "http://127.0.0.1:${host_port}${path}" || true) - [ "$response" = "403" ] || fail "Expected ${path} to return 403, got ${response:-}" -done +expect_storage_blocked pass "Web server blocks PHP execution under /storage" + +# The rest applies to FrankenPHP only: Caddy's log defaults and Laravel Octane. +[ -n "$(image_env CADDY_HTTP_PORT)" ] || exit 0 + +expect_authorization_redacted +expect_json_logs_on_stderr +pass "Web server writes JSON logs to stderr and redacts the authorization query parameter" + +# Octane starts FrankenPHP with LARAVEL_OCTANE=1 and the variables from +# src/Commands/StartFrankenPhpCommand.php in laravel/octane, which must switch +# /etc/frankenphp/Caddyfile to the Octane worker without losing the rest of the configuration. +cat > "$web_dir/frankenphp-worker.php" <<'PHP' +}" +pass "Octane mode exposes the Caddy admin API on Octane's admin port" + +expect_authorization_redacted +expect_json_logs_on_stderr +pass "Octane mode writes JSON logs to stderr and redacts the authorization query parameter" diff --git a/src/utilities-webservers/etc/entrypoint.d/5-generate-ssl.sh b/src/utilities-webservers/etc/entrypoint.d/5-generate-ssl.sh index 2b7dc5fe7..26fb92fd4 100644 --- a/src/utilities-webservers/etc/entrypoint.d/5-generate-ssl.sh +++ b/src/utilities-webservers/etc/entrypoint.d/5-generate-ssl.sh @@ -9,7 +9,14 @@ if [ "$DISABLE_DEFAULT_CONFIG" = "true" ]; then exit 0 fi -if [ "$SERVERSIDEUP_DEFAULT_COMMAND" != "true" ]; then +# FrankenPHP may be started by another command (like Laravel Octane), so SSL_MODE +# decides whether the key pair is needed there instead of the default command check +frankenphp_needs_ssl="false" +if [ -d "/etc/frankenphp/" ] && [ "${SSL_MODE:-off}" != "off" ]; then + frankenphp_needs_ssl="true" +fi + +if [ "$SERVERSIDEUP_DEFAULT_COMMAND" != "true" ] && [ "$frankenphp_needs_ssl" != "true" ]; then if [ "$LOG_OUTPUT_LEVEL" = "debug" ]; then echo "👉 $script_name: SERVERSIDEUP_DEFAULT_COMMAND is not true, so we won't generate a self-signed SSL key pair." fi diff --git a/src/variations/frankenphp/Dockerfile b/src/variations/frankenphp/Dockerfile index 2ec91e048..0163269ef 100644 --- a/src/variations/frankenphp/Dockerfile +++ b/src/variations/frankenphp/Dockerfile @@ -128,6 +128,9 @@ LABEL org.opencontainers.image.title="serversideup/php (frankenphp)" \ org.opencontainers.image.version="${REPOSITORY_BUILD_VERSION}" \ org.opencontainers.image.licenses="GPL-3.0-or-later" + # LARAVEL_OCTANE is intentionally not declared. The Caddyfile imports + # laravel-octane/*/{$LARAVEL_OCTANE:0}.caddyfile and Caddy only applies the + # default when the variable is unset, so declaring it (even as "") breaks startup. ENV APP_BASE_DIR=/var/www/html \ CADDY_ACME_PROFILE="off" \ CADDY_ADMIN="off" \ @@ -137,8 +140,8 @@ LABEL org.opencontainers.image.title="serversideup/php (frankenphp)" \ CADDY_HTTPS_PORT="8443" \ CADDY_HTTP_SERVER_ADDRESS="http://" \ CADDY_HTTPS_SERVER_ADDRESS="https://" \ - CADDY_LOG_FORMAT="console" \ - CADDY_LOG_OUTPUT="stdout" \ + CADDY_LOG_FORMAT="auto" \ + CADDY_LOG_OUTPUT="stderr" \ CADDY_PHP_SERVER_OPTIONS="" \ CADDY_SERVER_EXTRA_DIRECTIVES="" \ CADDY_SERVER_ROOT="/var/www/html/public" \ diff --git a/src/variations/frankenphp/etc/frankenphp/Caddyfile b/src/variations/frankenphp/etc/frankenphp/Caddyfile index 6359a0c23..288bb9d16 100644 --- a/src/variations/frankenphp/etc/frankenphp/Caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/Caddyfile @@ -2,8 +2,6 @@ # Global Caddy configuration ######################################################## { - admin {$CADDY_ADMIN:off} - import auto-https/{$CADDY_AUTO_HTTPS:off}.caddyfile http_port {$CADDY_HTTP_PORT:8080} @@ -15,8 +13,7 @@ frankenphp { {$FRANKENPHP_CONFIG} - {$CADDY_SERVER_WORKER_DIRECTIVE} - {$CADDY_SERVER_WATCH_DIRECTIVES} + import laravel-octane/frankenphp/{$LARAVEL_OCTANE:0}.caddyfile } import trusted-proxy/{$TRUSTED_PROXY:cloudflare}.caddyfile @@ -27,7 +24,9 @@ # Add additional Caddy configuration files from the caddyfile-global.d directory import caddyfile-global.d/*.caddyfile - {$CADDY_GLOBAL_OPTIONS} + # Laravel Octane sets LARAVEL_OCTANE=1 when it starts FrankenPHP, which switches + # the admin API, worker, and index file to what Octane expects + import laravel-octane/global/{$LARAVEL_OCTANE:0}.caddyfile } ######################################################## @@ -71,6 +70,7 @@ php_server { # FrankenPHP sets REMOTE_ADDR from the TCP peer, which ignores trusted_proxies env REMOTE_ADDR {client_ip} + import laravel-octane/php-server/{$LARAVEL_OCTANE:0}.caddyfile {$CADDY_PHP_SERVER_OPTIONS} } file_server diff --git a/src/variations/frankenphp/etc/frankenphp/caddyfile-global.d/README.caddyfile b/src/variations/frankenphp/etc/frankenphp/caddyfile-global.d/README.caddyfile new file mode 100644 index 000000000..a4a8ccc99 --- /dev/null +++ b/src/variations/frankenphp/etc/frankenphp/caddyfile-global.d/README.caddyfile @@ -0,0 +1,7 @@ +# Add your own .caddyfile files to this directory. They are imported inside the +# global options block of /etc/frankenphp/Caddyfile. +# Use caddyfile.d/ for site-level configuration instead. +# +# This file only exists so the import glob matches something. Caddy logs +# "No files matching import glob pattern" on every start when a directory +# like this one is empty. diff --git a/src/variations/frankenphp/etc/frankenphp/caddyfile.d/README.caddyfile b/src/variations/frankenphp/etc/frankenphp/caddyfile.d/README.caddyfile new file mode 100644 index 000000000..c92b9389b --- /dev/null +++ b/src/variations/frankenphp/etc/frankenphp/caddyfile.d/README.caddyfile @@ -0,0 +1,7 @@ +# Add your own .caddyfile files to this directory. They are imported at the top +# level of /etc/frankenphp/Caddyfile, so they can define extra site blocks. +# Use caddyfile-global.d/ for global options instead. +# +# This file only exists so the import glob matches something. Caddy logs +# "No files matching import glob pattern" on every start when a directory +# like this one is empty. diff --git a/src/variations/frankenphp/etc/frankenphp/laravel-octane/frankenphp/0.caddyfile b/src/variations/frankenphp/etc/frankenphp/laravel-octane/frankenphp/0.caddyfile new file mode 100644 index 000000000..a3015e2dd --- /dev/null +++ b/src/variations/frankenphp/etc/frankenphp/laravel-octane/frankenphp/0.caddyfile @@ -0,0 +1 @@ +# Laravel Octane is not running. Use FRANKENPHP_CONFIG to define workers. diff --git a/src/variations/frankenphp/etc/frankenphp/laravel-octane/frankenphp/1.caddyfile b/src/variations/frankenphp/etc/frankenphp/laravel-octane/frankenphp/1.caddyfile new file mode 100644 index 000000000..e712a7958 --- /dev/null +++ b/src/variations/frankenphp/etc/frankenphp/laravel-octane/frankenphp/1.caddyfile @@ -0,0 +1,9 @@ +# Same worker definition as Octane's own Caddyfile. Octane fills in the +# directives from --workers and --watch. The worker lives in CADDY_SERVER_ROOT +# so it always matches the "root" directive, even when Octane is not the +# process that started FrankenPHP. +worker { + file "{$CADDY_SERVER_ROOT:/var/www/html/public}/frankenphp-worker.php" + {$CADDY_SERVER_WORKER_DIRECTIVE} + {$CADDY_SERVER_WATCH_DIRECTIVES} +} diff --git a/src/variations/frankenphp/etc/frankenphp/laravel-octane/global/0.caddyfile b/src/variations/frankenphp/etc/frankenphp/laravel-octane/global/0.caddyfile new file mode 100644 index 000000000..0aec61fa7 --- /dev/null +++ b/src/variations/frankenphp/etc/frankenphp/laravel-octane/global/0.caddyfile @@ -0,0 +1,3 @@ +admin {$CADDY_ADMIN:off} + +{$CADDY_GLOBAL_OPTIONS} diff --git a/src/variations/frankenphp/etc/frankenphp/laravel-octane/global/1.caddyfile b/src/variations/frankenphp/etc/frankenphp/laravel-octane/global/1.caddyfile new file mode 100644 index 000000000..c5af5e042 --- /dev/null +++ b/src/variations/frankenphp/etc/frankenphp/laravel-octane/global/1.caddyfile @@ -0,0 +1,7 @@ +# octane:status, octane:reload, and octane:stop call the Caddy admin API on the +# host and port Octane passes in, so CADDY_ADMIN does not apply here +admin {$CADDY_SERVER_ADMIN_HOST:localhost}:{$CADDY_SERVER_ADMIN_PORT:2019} + +# CADDY_GLOBAL_OPTIONS is intentionally not used here. Octane sets it to +# "auto_https disable_redirects" for its own Caddyfile, which would override +# CADDY_AUTO_HTTPS. Use caddyfile-global.d/ for additional global options. diff --git a/src/variations/frankenphp/etc/frankenphp/laravel-octane/php-server/0.caddyfile b/src/variations/frankenphp/etc/frankenphp/laravel-octane/php-server/0.caddyfile new file mode 100644 index 000000000..f1ba7b30d --- /dev/null +++ b/src/variations/frankenphp/etc/frankenphp/laravel-octane/php-server/0.caddyfile @@ -0,0 +1 @@ +# Laravel Octane is not running. Requests are routed to index.php. diff --git a/src/variations/frankenphp/etc/frankenphp/laravel-octane/php-server/1.caddyfile b/src/variations/frankenphp/etc/frankenphp/laravel-octane/php-server/1.caddyfile new file mode 100644 index 000000000..112c2a080 --- /dev/null +++ b/src/variations/frankenphp/etc/frankenphp/laravel-octane/php-server/1.caddyfile @@ -0,0 +1,4 @@ +# Route requests to the Octane worker script instead of index.php. +# Octane's own Caddyfile also sets resolve_root_symlink, which FrankenPHP enables by default. +index frankenphp-worker.php +try_files {path} frankenphp-worker.php diff --git a/src/variations/frankenphp/etc/frankenphp/log-format/auto.caddyfile b/src/variations/frankenphp/etc/frankenphp/log-format/auto.caddyfile new file mode 100644 index 000000000..189542de1 --- /dev/null +++ b/src/variations/frankenphp/etc/frankenphp/log-format/auto.caddyfile @@ -0,0 +1,9 @@ +# No "wrap" here on purpose: Caddy picks console on an interactive terminal and +# json everywhere else, which is the default of the official FrankenPHP image. +# Mercure subscribers pass their JWT as ?authorization=, so redact it. Caddy +# nests the URI under "request", which is why the field is request>uri. +format filter { + request>uri query { + replace authorization REDACTED + } +} diff --git a/src/variations/frankenphp/etc/frankenphp/log-format/console.caddyfile b/src/variations/frankenphp/etc/frankenphp/log-format/console.caddyfile new file mode 100644 index 000000000..975e2f2a4 --- /dev/null +++ b/src/variations/frankenphp/etc/frankenphp/log-format/console.caddyfile @@ -0,0 +1,7 @@ +# Human-readable lines with colored levels, even when the output is not a terminal +format filter { + wrap console + request>uri query { + replace authorization REDACTED + } +} diff --git a/src/variations/frankenphp/etc/frankenphp/log-format/json.caddyfile b/src/variations/frankenphp/etc/frankenphp/log-format/json.caddyfile new file mode 100644 index 000000000..59a49e41b --- /dev/null +++ b/src/variations/frankenphp/etc/frankenphp/log-format/json.caddyfile @@ -0,0 +1,7 @@ +# One JSON object per line, even when the output is a terminal +format filter { + wrap json + request>uri query { + replace authorization REDACTED + } +} diff --git a/src/variations/frankenphp/etc/frankenphp/log-level/address/alert.caddyfile b/src/variations/frankenphp/etc/frankenphp/log-level/address/alert.caddyfile index 22f4e0619..94e521d99 100644 --- a/src/variations/frankenphp/etc/frankenphp/log-level/address/alert.caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/log-level/address/alert.caddyfile @@ -1,5 +1,5 @@ log { - format {$CADDY_LOG_FORMAT:console} - output {$CADDY_LOG_OUTPUT:stdout} + import ../../log-format/{$CADDY_LOG_FORMAT:auto}.caddyfile + output {$CADDY_LOG_OUTPUT:stderr} level {$CADDY_SERVER_LOG_LEVEL:ERROR} } diff --git a/src/variations/frankenphp/etc/frankenphp/log-level/address/crit.caddyfile b/src/variations/frankenphp/etc/frankenphp/log-level/address/crit.caddyfile index 22f4e0619..94e521d99 100644 --- a/src/variations/frankenphp/etc/frankenphp/log-level/address/crit.caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/log-level/address/crit.caddyfile @@ -1,5 +1,5 @@ log { - format {$CADDY_LOG_FORMAT:console} - output {$CADDY_LOG_OUTPUT:stdout} + import ../../log-format/{$CADDY_LOG_FORMAT:auto}.caddyfile + output {$CADDY_LOG_OUTPUT:stderr} level {$CADDY_SERVER_LOG_LEVEL:ERROR} } diff --git a/src/variations/frankenphp/etc/frankenphp/log-level/address/debug.caddyfile b/src/variations/frankenphp/etc/frankenphp/log-level/address/debug.caddyfile index bdb49c0ea..f760680ef 100644 --- a/src/variations/frankenphp/etc/frankenphp/log-level/address/debug.caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/log-level/address/debug.caddyfile @@ -1,5 +1,5 @@ log { - format {$CADDY_LOG_FORMAT:console} - output {$CADDY_LOG_OUTPUT:stdout} + import ../../log-format/{$CADDY_LOG_FORMAT:auto}.caddyfile + output {$CADDY_LOG_OUTPUT:stderr} level {$CADDY_SERVER_LOG_LEVEL:DEBUG} } diff --git a/src/variations/frankenphp/etc/frankenphp/log-level/address/emerg.caddyfile b/src/variations/frankenphp/etc/frankenphp/log-level/address/emerg.caddyfile index 22f4e0619..94e521d99 100644 --- a/src/variations/frankenphp/etc/frankenphp/log-level/address/emerg.caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/log-level/address/emerg.caddyfile @@ -1,5 +1,5 @@ log { - format {$CADDY_LOG_FORMAT:console} - output {$CADDY_LOG_OUTPUT:stdout} + import ../../log-format/{$CADDY_LOG_FORMAT:auto}.caddyfile + output {$CADDY_LOG_OUTPUT:stderr} level {$CADDY_SERVER_LOG_LEVEL:ERROR} } diff --git a/src/variations/frankenphp/etc/frankenphp/log-level/address/error.caddyfile b/src/variations/frankenphp/etc/frankenphp/log-level/address/error.caddyfile index 22f4e0619..94e521d99 100644 --- a/src/variations/frankenphp/etc/frankenphp/log-level/address/error.caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/log-level/address/error.caddyfile @@ -1,5 +1,5 @@ log { - format {$CADDY_LOG_FORMAT:console} - output {$CADDY_LOG_OUTPUT:stdout} + import ../../log-format/{$CADDY_LOG_FORMAT:auto}.caddyfile + output {$CADDY_LOG_OUTPUT:stderr} level {$CADDY_SERVER_LOG_LEVEL:ERROR} } diff --git a/src/variations/frankenphp/etc/frankenphp/log-level/address/info.caddyfile b/src/variations/frankenphp/etc/frankenphp/log-level/address/info.caddyfile index 0d972084b..3e31e2117 100644 --- a/src/variations/frankenphp/etc/frankenphp/log-level/address/info.caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/log-level/address/info.caddyfile @@ -1,5 +1,5 @@ log { - format {$CADDY_LOG_FORMAT:console} - output {$CADDY_LOG_OUTPUT:stdout} + import ../../log-format/{$CADDY_LOG_FORMAT:auto}.caddyfile + output {$CADDY_LOG_OUTPUT:stderr} level {$CADDY_SERVER_LOG_LEVEL:INFO} } diff --git a/src/variations/frankenphp/etc/frankenphp/log-level/address/notice.caddyfile b/src/variations/frankenphp/etc/frankenphp/log-level/address/notice.caddyfile index 0d972084b..3e31e2117 100644 --- a/src/variations/frankenphp/etc/frankenphp/log-level/address/notice.caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/log-level/address/notice.caddyfile @@ -1,5 +1,5 @@ log { - format {$CADDY_LOG_FORMAT:console} - output {$CADDY_LOG_OUTPUT:stdout} + import ../../log-format/{$CADDY_LOG_FORMAT:auto}.caddyfile + output {$CADDY_LOG_OUTPUT:stderr} level {$CADDY_SERVER_LOG_LEVEL:INFO} } diff --git a/src/variations/frankenphp/etc/frankenphp/log-level/address/warn.caddyfile b/src/variations/frankenphp/etc/frankenphp/log-level/address/warn.caddyfile index 4755f020e..5e98f9be9 100644 --- a/src/variations/frankenphp/etc/frankenphp/log-level/address/warn.caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/log-level/address/warn.caddyfile @@ -1,5 +1,5 @@ log { - format {$CADDY_LOG_FORMAT:console} - output {$CADDY_LOG_OUTPUT:stdout} + import ../../log-format/{$CADDY_LOG_FORMAT:auto}.caddyfile + output {$CADDY_LOG_OUTPUT:stderr} level {$CADDY_SERVER_LOG_LEVEL:WARN} } diff --git a/src/variations/frankenphp/etc/frankenphp/log-level/global/alert.caddyfile b/src/variations/frankenphp/etc/frankenphp/log-level/global/alert.caddyfile index 22f4e0619..94e521d99 100644 --- a/src/variations/frankenphp/etc/frankenphp/log-level/global/alert.caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/log-level/global/alert.caddyfile @@ -1,5 +1,5 @@ log { - format {$CADDY_LOG_FORMAT:console} - output {$CADDY_LOG_OUTPUT:stdout} + import ../../log-format/{$CADDY_LOG_FORMAT:auto}.caddyfile + output {$CADDY_LOG_OUTPUT:stderr} level {$CADDY_SERVER_LOG_LEVEL:ERROR} } diff --git a/src/variations/frankenphp/etc/frankenphp/log-level/global/crit.caddyfile b/src/variations/frankenphp/etc/frankenphp/log-level/global/crit.caddyfile index 22f4e0619..94e521d99 100644 --- a/src/variations/frankenphp/etc/frankenphp/log-level/global/crit.caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/log-level/global/crit.caddyfile @@ -1,5 +1,5 @@ log { - format {$CADDY_LOG_FORMAT:console} - output {$CADDY_LOG_OUTPUT:stdout} + import ../../log-format/{$CADDY_LOG_FORMAT:auto}.caddyfile + output {$CADDY_LOG_OUTPUT:stderr} level {$CADDY_SERVER_LOG_LEVEL:ERROR} } diff --git a/src/variations/frankenphp/etc/frankenphp/log-level/global/debug.caddyfile b/src/variations/frankenphp/etc/frankenphp/log-level/global/debug.caddyfile index 90124e286..aee25ad03 100644 --- a/src/variations/frankenphp/etc/frankenphp/log-level/global/debug.caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/log-level/global/debug.caddyfile @@ -1,6 +1,6 @@ debug log { - format {$CADDY_LOG_FORMAT:console} - output {$CADDY_LOG_OUTPUT:stdout} + import ../../log-format/{$CADDY_LOG_FORMAT:auto}.caddyfile + output {$CADDY_LOG_OUTPUT:stderr} level {$CADDY_SERVER_LOG_LEVEL:DEBUG} } diff --git a/src/variations/frankenphp/etc/frankenphp/log-level/global/emerg.caddyfile b/src/variations/frankenphp/etc/frankenphp/log-level/global/emerg.caddyfile index 22f4e0619..94e521d99 100644 --- a/src/variations/frankenphp/etc/frankenphp/log-level/global/emerg.caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/log-level/global/emerg.caddyfile @@ -1,5 +1,5 @@ log { - format {$CADDY_LOG_FORMAT:console} - output {$CADDY_LOG_OUTPUT:stdout} + import ../../log-format/{$CADDY_LOG_FORMAT:auto}.caddyfile + output {$CADDY_LOG_OUTPUT:stderr} level {$CADDY_SERVER_LOG_LEVEL:ERROR} } diff --git a/src/variations/frankenphp/etc/frankenphp/log-level/global/error.caddyfile b/src/variations/frankenphp/etc/frankenphp/log-level/global/error.caddyfile index 22f4e0619..94e521d99 100644 --- a/src/variations/frankenphp/etc/frankenphp/log-level/global/error.caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/log-level/global/error.caddyfile @@ -1,5 +1,5 @@ log { - format {$CADDY_LOG_FORMAT:console} - output {$CADDY_LOG_OUTPUT:stdout} + import ../../log-format/{$CADDY_LOG_FORMAT:auto}.caddyfile + output {$CADDY_LOG_OUTPUT:stderr} level {$CADDY_SERVER_LOG_LEVEL:ERROR} } diff --git a/src/variations/frankenphp/etc/frankenphp/log-level/global/info.caddyfile b/src/variations/frankenphp/etc/frankenphp/log-level/global/info.caddyfile index 0d972084b..3e31e2117 100644 --- a/src/variations/frankenphp/etc/frankenphp/log-level/global/info.caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/log-level/global/info.caddyfile @@ -1,5 +1,5 @@ log { - format {$CADDY_LOG_FORMAT:console} - output {$CADDY_LOG_OUTPUT:stdout} + import ../../log-format/{$CADDY_LOG_FORMAT:auto}.caddyfile + output {$CADDY_LOG_OUTPUT:stderr} level {$CADDY_SERVER_LOG_LEVEL:INFO} } diff --git a/src/variations/frankenphp/etc/frankenphp/log-level/global/notice.caddyfile b/src/variations/frankenphp/etc/frankenphp/log-level/global/notice.caddyfile index 0d972084b..3e31e2117 100644 --- a/src/variations/frankenphp/etc/frankenphp/log-level/global/notice.caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/log-level/global/notice.caddyfile @@ -1,5 +1,5 @@ log { - format {$CADDY_LOG_FORMAT:console} - output {$CADDY_LOG_OUTPUT:stdout} + import ../../log-format/{$CADDY_LOG_FORMAT:auto}.caddyfile + output {$CADDY_LOG_OUTPUT:stderr} level {$CADDY_SERVER_LOG_LEVEL:INFO} } diff --git a/src/variations/frankenphp/etc/frankenphp/log-level/global/warn.caddyfile b/src/variations/frankenphp/etc/frankenphp/log-level/global/warn.caddyfile index 4755f020e..5e98f9be9 100644 --- a/src/variations/frankenphp/etc/frankenphp/log-level/global/warn.caddyfile +++ b/src/variations/frankenphp/etc/frankenphp/log-level/global/warn.caddyfile @@ -1,5 +1,5 @@ log { - format {$CADDY_LOG_FORMAT:console} - output {$CADDY_LOG_OUTPUT:stdout} + import ../../log-format/{$CADDY_LOG_FORMAT:auto}.caddyfile + output {$CADDY_LOG_OUTPUT:stderr} level {$CADDY_SERVER_LOG_LEVEL:WARN} } From bd179751cedce9f64bbccab13c7adba299c301df Mon Sep 17 00:00:00 2001 From: Jay Rogers Date: Wed, 16 Sep 2026 18:53:00 +0000 Subject: [PATCH 3/3] Update FrankenPHP dependencies in Dockerfile to match specific plugin versions --- src/variations/frankenphp/Dockerfile | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/variations/frankenphp/Dockerfile b/src/variations/frankenphp/Dockerfile index 0163269ef..2705193e4 100644 --- a/src/variations/frankenphp/Dockerfile +++ b/src/variations/frankenphp/Dockerfile @@ -103,10 +103,12 @@ RUN if cat /etc/os-release | grep -q 'debian'; then \ --output /usr/local/bin/frankenphp \ --with github.com/dunglas/frankenphp=./ \ --with github.com/dunglas/frankenphp/caddy=./caddy/ \ - --with github.com/dunglas/caddy-cbrotli \ + # Plugin versions must match caddy/go.mod at the pinned FRANKENPHP_VERSION tag, + # otherwise xcaddy resolves the latest release, which may need a newer Go + --with github.com/dunglas/caddy-cbrotli@v1.0.1 \ # Mercure and Vulcain are included in the official build, but feel free to remove them - --with github.com/dunglas/mercure/caddy \ - --with github.com/dunglas/vulcain/caddy + --with github.com/dunglas/mercure/caddy@v0.24.2 \ + --with github.com/dunglas/vulcain/caddy@v1.4.2 #################### # FrankenPHP Final