Skip to content

Commit cd5b8a1

Browse files
authored
Pin Caddy to the version FrankenPHP's go.mod declares (#713)
1 parent 5201b80 commit cd5b8a1

2 files changed

Lines changed: 66 additions & 4 deletions

File tree

‎scripts/test-image.sh‎

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,17 @@ for pair in NGINX_HTTP_PORT:NGINX_WEBROOT APACHE_HTTP_PORT:APACHE_DOCUMENT_ROOT
8585
fi
8686
done
8787

88+
# The same images serve TLS on their HTTPS port once SSL_MODE is on, which is the only
89+
# way to reach them over HTTP/2.
90+
https_port=""
91+
for variable in NGINX_HTTPS_PORT APACHE_HTTPS_PORT CADDY_HTTPS_PORT; do
92+
https_port=$(image_env "$variable")
93+
[ -z "$https_port" ] || break
94+
done
95+
8896
# Web images run with OPcache enabled so the health check and the served page
8997
# cover the FPM and FrankenPHP SAPIs starting with the tuned defaults.
90-
run_args=(--detach --rm --env PHP_OPCACHE_ENABLE=1)
98+
run_args=(--detach --env PHP_OPCACHE_ENABLE=1)
9199
if [ -n "$http_port" ]; then
92100
# The container runs unprivileged, so the mounted document root must be world readable.
93101
web_dir=$(mktemp -d)
@@ -98,7 +106,18 @@ if [ -n "$http_port" ]; then
98106
chmod 755 "$web_dir/storage"
99107
echo '<?php echo "storage-php-executed";' > "$web_dir/storage/uploaded.php"
100108
chmod 644 "$web_dir/storage/uploaded.php"
109+
# Symfony's Response::send() calls fastcgi_finish_request() and Laravel keeps running
110+
# terminate callbacks after it, so the web server has to survive a request that finishes
111+
# before PHP does.
112+
cat > "$web_dir/finish-request.php" <<'PHP'
113+
<?php
114+
echo "finish-request-ok:" . PHP_VERSION;
115+
fastcgi_finish_request();
116+
usleep(50000);
117+
PHP
118+
chmod 644 "$web_dir/finish-request.php"
101119
run_args+=(--publish "127.0.0.1::${http_port}" --volume "$web_dir:$web_root:ro")
120+
[ -z "$https_port" ] || run_args+=(--publish "127.0.0.1::${https_port}")
102121
fi
103122

104123
containers=()
@@ -136,6 +155,7 @@ start_container() {
136155
fail "Container did not become healthy within ${health_timeout_seconds}s (status: $status)"
137156
fi
138157
[ -z "$http_port" ] || host_port=$(docker port "$container" "$http_port" | head -n1 | sed 's/.*://')
158+
[ -z "$https_port" ] || https_host_port=$(docker port "$container" "$https_port" | head -n1 | sed 's/.*://')
139159
}
140160

141161
# Retries until the response body matches, since the web server may still be warming up.
@@ -177,6 +197,32 @@ expect_authorization_redacted() {
177197
fail "Access log does not redact the authorization query parameter"
178198
}
179199

200+
# PHP carrying on after fastcgi_finish_request() has to leave the server able to take the
201+
# next request. Caddy 2.11.6 segfaulted the whole FrankenPHP process here, but only over
202+
# HTTP/2, so both protocol versions are worth checking.
203+
expect_work_after_finish_request() {
204+
for protocol in --http1.1 --http2; do
205+
body=""
206+
for _ in $(seq 1 "$http_timeout_seconds"); do
207+
body=$(curl --silent --insecure "$protocol" --max-time 5 "https://127.0.0.1:${https_host_port}/finish-request.php" 2>/dev/null || true)
208+
[ "$body" = "finish-request-ok:${php_version}" ] && break
209+
sleep 1
210+
done
211+
if [ "$body" != "finish-request-ok:${php_version}" ]; then
212+
dump_container_state "$container"
213+
fail "Web server did not serve /finish-request.php over ${protocol#--}. Response: ${body:-<empty>}"
214+
fi
215+
216+
# The crash lands after the response, so give the process a moment to fall over.
217+
sleep 2
218+
status=$(docker inspect --format '{{.State.Status}}' "$container" 2>/dev/null || echo gone)
219+
if [ "$status" != "running" ]; then
220+
dump_container_state "$container"
221+
fail "Web server stopped after a ${protocol#--} request finished before PHP did (status: $status)"
222+
fi
223+
done
224+
}
225+
180226
# A detached container has no terminal, so Caddy's default format is json and its default
181227
# stream is stderr. Octane depends on both: it only relays stderr and only parses JSON.
182228
expect_json_logs_on_stderr() {
@@ -202,6 +248,19 @@ pass "Web server serves PHP on port ${http_port}"
202248
expect_storage_blocked
203249
pass "Web server blocks PHP execution under /storage"
204250

251+
# HTTP/2 needs TLS, so this runs against its own container with SSL_MODE=full, which
252+
# generates a self-signed certificate and stops serving the plain HTTP port. The checks
253+
# after it still expect the container the rest of the suite has been using.
254+
if [ -n "$https_port" ]; then
255+
default_container="$container"
256+
default_host_port="$host_port"
257+
start_container --env SSL_MODE=full
258+
expect_work_after_finish_request
259+
pass "Web server survives a request that finishes before PHP does, over HTTP/1.1 and HTTP/2"
260+
container="$default_container"
261+
host_port="$default_host_port"
262+
fi
263+
205264
# The rest applies to FrankenPHP only: Caddy's log defaults and Laravel Octane.
206265
[ -n "$(image_env CADDY_HTTP_PORT)" ] || exit 0
207266

‎src/variations/frankenphp/Dockerfile‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ ARG BASE_OS_VERSION='trixie'
33
ARG PHP_VERSION='8.5'
44
ARG BASE_IMAGE="php:${PHP_VERSION}-zts-${BASE_OS_VERSION}"
55
ARG FRANKENPHP_VERSION='1.12.7'
6+
ARG CADDY_VERSION='v2.11.4'
67
ARG GOLANG_VERSION='1.26'
78

89
########################
@@ -51,6 +52,7 @@ FROM golang:${GOLANG_VERSION} AS golang-image
5152
####################
5253
FROM common AS frankenphp-build
5354
ARG FRANKENPHP_VERSION
55+
ARG CADDY_VERSION
5456
ARG GOLANG_VERSION
5557
ARG BUILD_DEPENDENCY_PACKAGES_ALPINE="\
5658
argon2-dev \
@@ -133,12 +135,13 @@ RUN if cat /etc/os-release | grep -q 'debian'; then \
133135
XCADDY_GO_BUILD_FLAGS="-ldflags='-w -s' -tags=nobadger,nomysql,nopgx" \
134136
CGO_CFLAGS="-DFRANKENPHP_VERSION=${FRANKENPHP_VERSION} $(php-config --includes) $ADDITIONAL_BUILD_FLAGS" \
135137
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
136-
xcaddy build \
138+
# CADDY_VERSION and the plugin versions must match caddy/go.mod at the pinned
139+
# FRANKENPHP_VERSION tag, otherwise xcaddy resolves the latest release, which
140+
# may need a newer Go or ship a regression upstream never tested against
141+
xcaddy build "${CADDY_VERSION}" \
137142
--output /usr/local/bin/frankenphp \
138143
--with github.com/dunglas/frankenphp=./ \
139144
--with github.com/dunglas/frankenphp/caddy=./caddy/ \
140-
# Plugin versions must match caddy/go.mod at the pinned FRANKENPHP_VERSION tag,
141-
# otherwise xcaddy resolves the latest release, which may need a newer Go
142145
--with github.com/dunglas/caddy-cbrotli@v1.0.1 \
143146
# Mercure and Vulcain are included in the official build, but feel free to remove them
144147
--with github.com/dunglas/mercure/caddy@v0.24.2 \

0 commit comments

Comments
 (0)