From c7befe07b49abf2c198cd3e59c9bf40c67d74c7d Mon Sep 17 00:00:00 2001 From: Lorenzo Rogai Date: Wed, 22 Jul 2026 15:54:04 +0200 Subject: [PATCH] fix(s6): make web services wait for their config oneshots to fix root-mode startup race When a container built on the s6 images runs as root, php-fpm and the web server (nginx/apache2) are brought up in parallel with the entrypoint oneshots that configure them, because the long-running services have no dependency on those oneshots. As root this races: - php-fpm reads its pool before `5-fpm-pool-user` appends `user`/`group`, failing with "ALERT: [pool www] user has not been defined" -> "ERROR: FPM initialization failed". - the web server starts before `10-init-webserver-config` renders its config (e.g. nginx: open() "/etc/nginx/nginx.conf" failed). s6 restarts the crashed services so the container eventually recovers, which is why the failure is intermittent and hard to reproduce (see discussion #425), but it emits alarming errors, slows startup, and leaves a brief window with no service. docker-php-serversideup-s6-init now adds a dependency from each web service to the entrypoint oneshot that configures it, appending to the existing flat `dependencies` file. The oneshots are chained in alphabetical order, so depending on one transitively waits for all earlier ones (php-fpm -> 5-fpm-pool-user; nginx/apache2 -> 10-init-webserver-config). Entries are de-duplicated and appended newline-safely (nginx's shipped `dependencies` has no trailing newline). Dependencies are only added when both the service and the oneshot exist, so cli/fpm/frankenphp images and images that remove a script are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../local/bin/docker-php-serversideup-s6-init | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/s6/usr/local/bin/docker-php-serversideup-s6-init b/src/s6/usr/local/bin/docker-php-serversideup-s6-init index d61307140..6afdf5b6e 100644 --- a/src/s6/usr/local/bin/docker-php-serversideup-s6-init +++ b/src/s6/usr/local/bin/docker-php-serversideup-s6-init @@ -77,4 +77,37 @@ for file in "$ENTRYPOINT_DIR"/*.sh; do echo "Skipping ${script_name} because it already exists at ${S6_HOME}/scripts/${script_name}" fi -done \ No newline at end of file +done + +# Make the long-running services wait for the entrypoint oneshots that configure +# them. When the container runs as root, php-fpm and the web server otherwise +# start in parallel with these oneshots and can lose the race: php-fpm reads the +# pool before "5-fpm-pool-user" adds "user = www-data" (ALERT: [pool www] user +# has not been defined -> FPM initialization failed), and the web server starts +# before "10-init-webserver-config" renders its config. s6 restarts the crashed +# services so the container recovers, but it produces alarming errors, a slower +# start, and a brief window with no service. The entrypoint oneshots are chained +# in alphabetical order, so depending on one transitively waits for all earlier +# ones. Each dependency is only added when both the service and the oneshot exist. +add_startup_dependency() { + # $1 = long-running service that must wait, $2 = entrypoint oneshot it needs + service_dir="${S6_HOME}/s6-rc.d/$1" + [ -d "$service_dir" ] && [ -d "${S6_HOME}/s6-rc.d/$2" ] || return 0 + + dependencies_file="${service_dir}/dependencies" + [ -e "$dependencies_file" ] || : > "$dependencies_file" + + # Skip if the dependency is already declared + grep -qxF "$2" "$dependencies_file" 2>/dev/null && return 0 + + # Ensure existing content ends with a newline before appending (some shipped + # dependency files, e.g. nginx's, have no trailing newline) + if [ -s "$dependencies_file" ] && [ -n "$(tail -c 1 "$dependencies_file")" ]; then + printf '\n' >> "$dependencies_file" + fi + printf '%s\n' "$2" >> "$dependencies_file" +} + +add_startup_dependency php-fpm 5-fpm-pool-user +add_startup_dependency nginx 10-init-webserver-config +add_startup_dependency apache2 10-init-webserver-config \ No newline at end of file