From 9bf5267eff39341c94dc0ea396b08fb5988dd960 Mon Sep 17 00:00:00 2001 From: Jay Rogers Date: Sat, 3 Oct 2026 11:31:46 +0000 Subject: [PATCH 1/2] Pin Caddy to the version FrankenPHP's go.mod declares xcaddy build with no version resolves the latest Caddy release, which overrides the pin in caddy/go.mod at the FRANKENPHP_VERSION tag. Caddy 2.11.6 segfaults the worker on HTTP/2 when PHP keeps running after fastcgi_finish_request(), which takes down every Laravel app behind HTTP/2 or a Traefik backend. Fixes #712 --- src/variations/frankenphp/Dockerfile | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/variations/frankenphp/Dockerfile b/src/variations/frankenphp/Dockerfile index fbd35712..1d026dc7 100644 --- a/src/variations/frankenphp/Dockerfile +++ b/src/variations/frankenphp/Dockerfile @@ -3,6 +3,7 @@ ARG BASE_OS_VERSION='trixie' ARG PHP_VERSION='8.5' ARG BASE_IMAGE="php:${PHP_VERSION}-zts-${BASE_OS_VERSION}" ARG FRANKENPHP_VERSION='1.12.7' +ARG CADDY_VERSION='v2.11.4' ARG GOLANG_VERSION='1.26' ######################## @@ -51,6 +52,7 @@ FROM golang:${GOLANG_VERSION} AS golang-image #################### FROM common AS frankenphp-build ARG FRANKENPHP_VERSION +ARG CADDY_VERSION ARG GOLANG_VERSION ARG BUILD_DEPENDENCY_PACKAGES_ALPINE="\ argon2-dev \ @@ -133,12 +135,13 @@ RUN if cat /etc/os-release | grep -q 'debian'; then \ XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \ CGO_CFLAGS="-DFRANKENPHP_VERSION=${FRANKENPHP_VERSION} $(php-config --includes) $ADDITIONAL_BUILD_FLAGS" \ CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \ - xcaddy build \ + # CADDY_VERSION and the 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 or ship a regression upstream never tested against + xcaddy build "${CADDY_VERSION}" \ --output /usr/local/bin/frankenphp \ --with github.com/dunglas/frankenphp=./ \ --with github.com/dunglas/frankenphp/caddy=./caddy/ \ - # 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@v0.24.2 \ From a0760255ee2029a3c8ff875e68cbc4d4135d65d7 Mon Sep 17 00:00:00 2001 From: Jay Rogers Date: Sat, 3 Oct 2026 11:56:15 +0000 Subject: [PATCH 2/2] Check that the web server survives a request finishing before PHP does Nothing in the suite called fastcgi_finish_request() and then kept working, so the Caddy 2.11.6 crash reached beta3 unnoticed. The check runs against its own container with SSL_MODE=full, since HTTP/2 needs TLS and that mode stops serving the plain HTTP port, and it covers NGINX, Apache and FrankenPHP through their *_HTTPS_PORT variables. Containers no longer start with --rm. cleanup() already removes everything the script starts, while --rm discarded the logs of a container that crashed, which is exactly when they are worth reading. --- scripts/test-image.sh | 61 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/scripts/test-image.sh b/scripts/test-image.sh index 74d8f575..80ca1c31 100755 --- a/scripts/test-image.sh +++ b/scripts/test-image.sh @@ -85,9 +85,17 @@ for pair in NGINX_HTTP_PORT:NGINX_WEBROOT APACHE_HTTP_PORT:APACHE_DOCUMENT_ROOT fi done +# The same images serve TLS on their HTTPS port once SSL_MODE is on, which is the only +# way to reach them over HTTP/2. +https_port="" +for variable in NGINX_HTTPS_PORT APACHE_HTTPS_PORT CADDY_HTTPS_PORT; do + https_port=$(image_env "$variable") + [ -z "$https_port" ] || break +done + # Web images run with OPcache enabled so the health check and the served page # cover the FPM and FrankenPHP SAPIs starting with the tuned defaults. -run_args=(--detach --rm --env PHP_OPCACHE_ENABLE=1) +run_args=(--detach --env PHP_OPCACHE_ENABLE=1) if [ -n "$http_port" ]; then # The container runs unprivileged, so the mounted document root must be world readable. web_dir=$(mktemp -d) @@ -98,7 +106,18 @@ if [ -n "$http_port" ]; then chmod 755 "$web_dir/storage" echo ' "$web_dir/storage/uploaded.php" chmod 644 "$web_dir/storage/uploaded.php" + # Symfony's Response::send() calls fastcgi_finish_request() and Laravel keeps running + # terminate callbacks after it, so the web server has to survive a request that finishes + # before PHP does. + cat > "$web_dir/finish-request.php" <<'PHP' +/dev/null || true) + [ "$body" = "finish-request-ok:${php_version}" ] && break + sleep 1 + done + if [ "$body" != "finish-request-ok:${php_version}" ]; then + dump_container_state "$container" + fail "Web server did not serve /finish-request.php over ${protocol#--}. Response: ${body:-}" + fi + + # The crash lands after the response, so give the process a moment to fall over. + sleep 2 + status=$(docker inspect --format '{{.State.Status}}' "$container" 2>/dev/null || echo gone) + if [ "$status" != "running" ]; then + dump_container_state "$container" + fail "Web server stopped after a ${protocol#--} request finished before PHP did (status: $status)" + fi + done +} + # 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() { @@ -202,6 +248,19 @@ pass "Web server serves PHP on port ${http_port}" expect_storage_blocked pass "Web server blocks PHP execution under /storage" +# HTTP/2 needs TLS, so this runs against its own container with SSL_MODE=full, which +# generates a self-signed certificate and stops serving the plain HTTP port. The checks +# after it still expect the container the rest of the suite has been using. +if [ -n "$https_port" ]; then + default_container="$container" + default_host_port="$host_port" + start_container --env SSL_MODE=full + expect_work_after_finish_request + pass "Web server survives a request that finishes before PHP does, over HTTP/1.1 and HTTP/2" + container="$default_container" + host_port="$default_host_port" +fi + # The rest applies to FrankenPHP only: Caddy's log defaults and Laravel Octane. [ -n "$(image_env CADDY_HTTP_PORT)" ] || exit 0