S6 v3 for PHP (+ Caddy/Nginx)

Have you tried to setup an S6 supervisor for the official PHP Docker image, just to realize that something does not work right? Like, the S6 does not react to container termination command?

Here’s what happened to me.

The context

My initial setup was a docker-compose.yml configuration file with php as backend service, and nginx as the app server, among other services.

The idea was to replace Nginx with Caddy, and at the same time to use the famous S6 as a supervisor in the PHP image, getting rid of the server service (since it will be bundled in the PHP image). So, I remove the server service part from docker-compose configuration, and the full focus is now on the PHP image.

I’ve started by adding the Caddy server to the image, then s6-overlay binaries, following the change of the ENTRYPOINT. Next, I followed the guide to add service definitions for S6 in the v3 format.

It worked “almost” perfect. The only issue I have encountered is that when I stop the services (e.g. docker-compose stop) the container gets killed after a timeout of 10 seconds, which is the default behavior for the container termination. So it looked like S6 did not receive any signal.

Few hours later I decide it’s time to follow the step by step example, just to see the S6 working as expected. Then I can gradually change things, and test the new configuration. This would help me narrow down the issues spectrum, and find out the issue for my original build configuration.

A fresh start with S6

Following the Quick start guide from the s6-overlay repository, I threw the Dockerfile content into github.dockerfile file, then crafted a Makefile to ease test process.

# github.dockerfile
FROM ubuntu
ARG S6_OVERLAY_VERSION=3.1.0.1

RUN apt-get update && apt-get install -y nginx xz-utils
RUN echo "daemon off;" >> /etc/nginx/nginx.conf
CMD ["/usr/sbin/nginx"]

ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz /tmp
RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz
ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-x86_64.tar.xz /tmp
RUN tar -C / -Jxpf /tmp/s6-overlay-x86_64.tar.xz
ENTRYPOINT ["/init"]
# Makefile
run-github:
	docker build --build-arg=S6_OVERLAY_VERSION=3.1.0.1 -t s6-github-example -f ./github.dockerfile ./.
	docker run --rm --name s6demo -d -p 8080:80 s6-github-example:latest
	docker logs -ft s6demo

stop:
	docker stop s6demo

After running the make run-github command I get the expected output in my terminal.

...
docker logs -ft s6demo
s6-rc: info: service s6rc-oneshot-runner: starting
s6-rc: info: service s6rc-oneshot-runner successfully started
s6-rc: info: service fix-attrs: starting
s6-rc: info: service fix-attrs successfully started
s6-rc: info: service legacy-cont-init: starting
s6-rc: info: service legacy-cont-init successfully started
s6-rc: info: service legacy-services: starting
s6-rc: info: service legacy-services successfully started

In another terminal I ran make stop and again, I get the expected output in logs (in the previous terminal):

...
s6-rc: info: service legacy-services: stopping
s6-rc: info: service legacy-services successfully stopped
s6-rc: info: service legacy-cont-init: stopping
s6-rc: info: service legacy-cont-init successfully stopped
s6-rc: info: service fix-attrs: stopping
s6-rc: info: service fix-attrs successfully stopped
s6-rc: info: service s6rc-oneshot-runner: stopping
s6-rc: info: service s6rc-oneshot-runner successfully stopped

So I thought to try next to run the same instructions, but instead of building from ubuntu:latest to use php:8.1-fpm-alpine3.15 (with small adjustments). The php-original.dockerfile would look like this:

# php-original.dockerfile
FROM php:8.1-fpm-alpine3.15
ARG S6_OVERLAY_VERSION=3.1.0.1

RUN apk update && apk add nginx
RUN echo "daemon off;" >> /etc/nginx/nginx.conf && which nginx
CMD ["/usr/sbin/nginx"]

ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz /tmp
RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz
ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-x86_64.tar.xz /tmp
RUN tar -C / -Jxpf /tmp/s6-overlay-x86_64.tar.xz
ENTRYPOINT ["/init"]
CMD []
# Makefile
# ...
run-php-original:
	docker build --build-arg=S6_OVERLAY_VERSION=3.1.0.1 -t s6-php-original-alpine -f ./php-original.dockerfile ./.
	docker run --rm --name s6demo -d -p 8080:80 s6-php-original-alpine:latest
	docker logs -ft s6demo
# ...

This time, by running make run-php-original and make stop I get the following output:

s6-rc: info: service s6rc-oneshot-runner: starting
s6-rc: info: service s6rc-oneshot-runner successfully started
s6-rc: info: service fix-attrs: starting
s6-rc: info: service fix-attrs successfully started
s6-rc: info: service legacy-cont-init: starting
s6-rc: info: service legacy-cont-init successfully started
s6-rc: info: service legacy-services: starting
s6-rc: info: service legacy-services successfully started

The container was killed after the 10 seconds of grace period after the SIGTERM signal was fired. It was like the S6 PID #1 process did not catch that signal at all.

Digging further I found put that the default terminate signal was replaced in PHP Docker image by default, and for a good reason – to gracefully stop all active PHP-FPM workers. So instead of SIGTERM, the PHP-fpm process would receive a SIGINT now.

And that’s what I’ll try next, in the php-stop-signal.dockerfile: add a STOPSIGNAL SIGTERM line at the end, to restore the default terminate signal, in an attempt to get the expected behavior from S6.

# php-stop-signal.dockerfile
FROM php:8.1-fpm-alpine3.15
ARG S6_OVERLAY_VERSION=3.1.0.1

RUN apk update && apk add nginx
RUN echo "daemon off;" >> /etc/nginx/nginx.conf && which nginx
CMD ["/usr/sbin/nginx"]

ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-noarch.tar.xz /tmp
RUN tar -C / -Jxpf /tmp/s6-overlay-noarch.tar.xz
ADD https://github.com/just-containers/s6-overlay/releases/download/v${S6_OVERLAY_VERSION}/s6-overlay-x86_64.tar.xz /tmp
RUN tar -C / -Jxpf /tmp/s6-overlay-x86_64.tar.xz
ENTRYPOINT ["/init"]
CMD []
STOPSIGNAL SIGTERM
# Makefile
# ...
run-php-stop-signal:
	docker build --build-arg=S6_OVERLAY_VERSION=3.1.0.1 -t s6-php-stop-signal-alpine -f ./php-stop-signal.dockerfile ./.
	docker run --rm --name s6demo -d -p 8080:80 s6-php-stop-signal-alpine:latest
	docker logs -ft s6demo
# ...

After make run-php-stop-signal and make stop I finally get the expected output and behavior:

s6-rc: info: service s6rc-oneshot-runner: starting
s6-rc: info: service s6rc-oneshot-runner successfully started
s6-rc: info: service fix-attrs: starting
s6-rc: info: service fix-attrs successfully started
s6-rc: info: service legacy-cont-init: starting
s6-rc: info: service legacy-cont-init successfully started
s6-rc: info: service legacy-services: starting
s6-rc: info: service legacy-services successfully started
s6-rc: info: service legacy-services: stopping
s6-rc: info: service legacy-services successfully stopped
s6-rc: info: service legacy-cont-init: stopping
s6-rc: info: service legacy-cont-init successfully stopped
s6-rc: info: service fix-attrs: stopping
s6-rc: info: service fix-attrs successfully stopped
s6-rc: info: service s6rc-oneshot-runner: stopping
s6-rc: info: service s6rc-oneshot-runner successfully stopped

That’s great, but wait…

“I’m loosing a great feature I did not care about before 1 minute ago.” The graceful stop from PHP-FPM workers, right? Well, S6 configuration can help you with that.

I’m new to S6, and I choose to apply the new configuration format (v3). (Who wants to learn new stuff which is deprecated, anyway?). So I’ve configured my long-run services into files, according to specification. The signal to pass to the long-run service should reside in the file called down-signal (where I put the SIGQUIT, of course).

S6 has tons of options that can help you not only configure the runtime services and inter-dependencies, but also setup your app in a structured manner, among others. My typical setup for an application that runs Caddy server + PHP-FPM + setups would look like this:

S6 setup for Caddy + PHP-FPM + setups

I’m so glad I managed to configure S6 to run a server with PHP-FPM in the same container! 🎉

I hope you find this useful.

Leave a Reply

Your email address will not be published. Required fields are marked *