Prevent S6 startup races: services wait for their config oneshots and depend on base - #689
Conversation
…-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 serversideup#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) <noreply@anthropic.com>
46d9ba2 to
c7befe0
Compare
…-mode-fpm-nginx-startup-race
…-mode-fpm-nginx-startup-race
…lat dependencies file to dependencies.d, make every user service depend on base as s6-overlay recommends, and make php-fpm, nginx, and apache2 wait for the oneshots that write their config. Fixes root-mode FPM 'user has not been defined' race (serversideup#425, serversideup#689). Co-authored-by: LorenzoRogai <1665768+LorenzoRogai@users.noreply.github.com> Co-authored-by: mbrodala <5037116+mbrodala@users.noreply.github.com>
|
Thanks @LorenzoRogai and @mbrodala! We definitely had a problem here and I want to thank you both for helping me identify and resolve this. I noticed a few other things so I made a bigger refactor (289b4b0) of this process and gave you both as credit. Here's why: Why the format changedWhile verifying the fix I checked the s6-overlay README and the s6-rc docs. The flat Why
|
I made a bigger refactor and explained why here #689 (comment)
24e60f9
into
serversideup:release/webserver-improvements-and-fixes
|
@jaydrogers thanks for the update! Will this require any changes on our side if we’re using custom s6 scripts? |
|
Yes, I ahve that added here: #645
Let me know if you have any other questions. Thanks again! |

Note
Note from @jaydrogers: I took over this PR to finish it and rewrote this description to match our PR format. The scope grew beyond the original patch (see my comment below for why). @LorenzoRogai's original description is preserved at the bottom of this comment.
Why we created this PR
When a container built from our S6 images runs as root (for example, following the advice in #425),
php-fpmsometimes fails on its first start:S6 restarts it and the container recovers, so it looks like noise. It's a race. PHP-FPM refuses to start as root without a
userin the pool config. Our5-fpm-pool-useroneshot writes that line, butphp-fpmnever declared that it depends on the oneshot, so S6 started both at once. On the same boot,nginxandapache2could also start before10-init-webserver-configrendered their config.Rootless containers never hit it because PHP-FPM ignores the
userdirective when it isn't root. Onmain, the error reproduces on every root-mode boot.While fixing the dependency, we found two more things that S6 Overlay documents and we weren't doing:
dependenciesfile. The s6-overlay README only documentsdependencies.d/directories, and s6-rc marks the flat file deprecated. With the directory format, a dependency is one empty file, so the whole fix is atouch.base. The README is direct about it: services that don't depend onbase"might have been started earlier, which may cause race conditions - so it's recommended to always make them depend onbase."How to test
View the testing images →
To see the race yourself, build this against
serversideup/php:8.4-fpm-nginx-alpine(fails) and against a689-image (passes):What this PR does
Startup order
php-fpmnow waits for5-fpm-pool-user;nginxandapache2wait for10-init-webserver-configandphp-fpmbase, as S6 Overlay recommendsBefore (
main):php-fpm,nginx, andapache2start the moment S6 is up, in parallel with the oneshots that write their config.flowchart LR subgraph oneshots [Entrypoint oneshots] a[0-container-info] --> b[1-log-output-level] b --> c[5-fpm-pool-user] b --> d[10-init-webserver-config] d --> e[50-laravel-automations] end subgraph services [Long-running services] fpm[php-fpm] --> web[nginx / apache2] end c -. race .- fpm d -. race .- webAfter: every service and oneshot depends on
base, and each service waits for the oneshot that configures it.flowchart LR base --> a[0-container-info] --> b[1-log-output-level] b --> c[5-fpm-pool-user] --> fpm[php-fpm] b --> d[10-init-webserver-config] --> e[50-laravel-automations] d --> web[nginx / apache2] fpm --> webEvery node above also lists
basedirectly in itsdependencies.d/; those edges are omitted to keep the diagram readable.S6 Overlay alignment
dependenciesfiles forphp-fpm,nginx, andapache2withdependencies.d/directoriesdocker-php-serversideup-s6-initnow has oneadd_dependencyhelper (amkdir -pand atouch) used for oneshot chaining and for the service dependencies aboveDocs
dependencies.dandbaseguidance to Adding your own start-up scriptsCompatibility
Stock images and rootless containers behave the same. If you appended lines to our old flat
dependenciesfiles in your own Dockerfile, S6 now ignores those files oncedependencies.d/exists. Move each line to an empty file independencies.d/instead.Original description by @LorenzoRogai
Problem
When a container built on the s6 images runs as root,
php-fpmand the web server (nginx/apache2) are brought up in parallel with the entrypoint oneshots that configure them — the long-running services declare no dependency on those oneshots.As root this is a race:
php-fpmreads its pool config before5-fpm-pool-userappendsuser/group, failing with:10-init-webserver-configrenders its config, e.g.nginx: [emerg] open() "/etc/nginx/nginx.conf" failed (2: No such file or directory).s6restarts the crashed longruns, so the container eventually recovers — which is exactly why this is intermittent and hard to reproduce (see #425). But it emits alarming errors on every boot that loses the race, slows startup, and leaves a brief window with no service.Fix
docker-php-serversideup-s6-initnow adds a dependency from each web-facing longrun to the entrypoint oneshot that configures it, after the oneshots are created:The entrypoint oneshots are chained in alphabetical order, so depending on one transitively waits for all earlier ones (
php-fpm→5-fpm-pool-user; the web servers →10-init-webserver-config, which already sits after5-*). Each dependency is added only when both the service and the oneshot exist, socli/fpm/frankenphpimages — and images where a user removes a script — are unaffected. It does not make services wait on later app oneshots such as50-laravel-automations, so a failing app hook won't block the web server from starting.Testing
8.4-fpm-nginx-alpine: first boot shows the fpm + nginx errors, then self-heals.php-fpm/nginxreach "ready to handle connections" on the first attempt.5-fpm-pool-useris a no-op when not root; the dependency just orders startup).Refs #425