What happened?
With shutdown_delay 30s, PHP requests return HTTP 500 after SIGTERM while Caddy continues serving static responses. The error is FrankenPHP is not running. This reproduces with an unmodified official image and a trivial PHP script, without a framework or Kubernetes.
Caddy documents that the server continues operating normally during shutdown_delay, before the grace period begins. I expected PHP requests to remain serviceable during that delay, while the health endpoint returns 503.
Reproduced on FrankenPHP 1.12.6 and 1.12.7, PHP 8.5.9, Caddy 2.11.4, linux/arm64, using the Debian Trixie Docker images. The 1.12.7 image digest was sha256:f92d81eb3fe4fd18b35d3d58192b7cc3acc8943817bbf39f2fcf0be02a3916dc.
Reproduction
Save this as Caddyfile:
{
admin localhost:2019
auto_https off
shutdown_delay 30s
grace_period 300s
frankenphp {
num_threads 2
}
}
:8080 {
log
handle /health {
@stopping vars {http.shutting_down} true
respond @stopping "stopping" 503
respond "healthy" 200
}
handle /static {
respond "static-ok" 200
}
handle {
root * /app/public
php_server
}
}
Save this as index.php:
<?php
header('Content-Type: text/plain');
echo "php-ok\n";
Start the container:
container=$(docker run -d -p 127.0.0.1::8080 \
-v "$PWD/Caddyfile:/etc/caddy/Caddyfile:ro" \
-v "$PWD/index.php:/app/public/index.php:ro" \
--entrypoint frankenphp \
dunglas/frankenphp:1.12.7-php8.5.9-trixie \
run --config /etc/caddy/Caddyfile)
endpoint=$(docker port "$container" 8080/tcp)
Once curl -i "http://$endpoint/" returns 200 and php-ok, send SIGTERM and immediately request all three endpoints:
docker kill --signal=TERM "$container"
curl -i "http://$endpoint/"
curl -i "http://$endpoint/static"
curl -i "http://$endpoint/health"
docker logs "$container"
Observed: PHP returns 500, static returns 200, and health returns 503. In the automated 1.12.7 run, PHP returned 79 sampled 500 responses throughout the 30-second delay; each was followed by a successful static request. The container then exited normally. Cleanup: docker rm -f "$container".
If the first shutdown does not reproduce, repeat with a fresh container; the app-stop order can differ between runs.
Suspected cause and local experiment
FrankenPHPApp.Stop() calls frankenphp.Shutdown() when Caddy is exiting, while Caddy stops its apps without dependency ordering. PHP can therefore stop before the HTTP app finishes its shutdown delay.
I tested moving PHP shutdown to a single process-global caddy.OnExit callback. On both releases, the local patch eliminated these 500s and the process exited cleanly. A 40-second PHP request already running at SIGTERM also completed successfully across the 30-second delay, including after two configuration reloads. That test explicitly allowed 90 seconds of PHP execution time. Worker-mode behavior was not tested.
Is deferring PHP shutdown until after the HTTP app has stopped the appropriate fix here?
Build Type
Docker (Debian Trixie)
Worker Mode
No
Operating System / CPU Architecture
GNU/Linux container on macOS/OrbStack; aarch64.
PHP configuration
Official image defaults, with no custom PHP configuration or additional extensions for the minimal reproduction.
What happened?
With
shutdown_delay 30s, PHP requests return HTTP 500 after SIGTERM while Caddy continues serving static responses. The error isFrankenPHP is not running. This reproduces with an unmodified official image and a trivial PHP script, without a framework or Kubernetes.Caddy documents that the server continues operating normally during
shutdown_delay, before the grace period begins. I expected PHP requests to remain serviceable during that delay, while the health endpoint returns 503.Reproduced on FrankenPHP 1.12.6 and 1.12.7, PHP 8.5.9, Caddy 2.11.4, linux/arm64, using the Debian Trixie Docker images. The 1.12.7 image digest was
sha256:f92d81eb3fe4fd18b35d3d58192b7cc3acc8943817bbf39f2fcf0be02a3916dc.Reproduction
Save this as
Caddyfile:{ admin localhost:2019 auto_https off shutdown_delay 30s grace_period 300s frankenphp { num_threads 2 } } :8080 { log handle /health { @stopping vars {http.shutting_down} true respond @stopping "stopping" 503 respond "healthy" 200 } handle /static { respond "static-ok" 200 } handle { root * /app/public php_server } }Save this as
index.php:Start the container:
Once
curl -i "http://$endpoint/"returns 200 andphp-ok, send SIGTERM and immediately request all three endpoints:Observed: PHP returns 500, static returns 200, and health returns 503. In the automated 1.12.7 run, PHP returned 79 sampled 500 responses throughout the 30-second delay; each was followed by a successful static request. The container then exited normally. Cleanup:
docker rm -f "$container".If the first shutdown does not reproduce, repeat with a fresh container; the app-stop order can differ between runs.
Suspected cause and local experiment
FrankenPHPApp.Stop()callsfrankenphp.Shutdown()when Caddy is exiting, while Caddy stops its apps without dependency ordering. PHP can therefore stop before the HTTP app finishes its shutdown delay.I tested moving PHP shutdown to a single process-global
caddy.OnExitcallback. On both releases, the local patch eliminated these 500s and the process exited cleanly. A 40-second PHP request already running at SIGTERM also completed successfully across the 30-second delay, including after two configuration reloads. That test explicitly allowed 90 seconds of PHP execution time. Worker-mode behavior was not tested.Is deferring PHP shutdown until after the HTTP app has stopped the appropriate fix here?
Build Type
Docker (Debian Trixie)
Worker Mode
No
Operating System / CPU Architecture
GNU/Linux container on macOS/OrbStack; aarch64.
PHP configuration
Official image defaults, with no custom PHP configuration or additional extensions for the minimal reproduction.