From d0e16dc1c58d241d9e716757530dfaa5d62d0fe2 Mon Sep 17 00:00:00 2001 From: Stefan van Essen Date: Fri, 30 Jan 2026 01:45:30 +0100 Subject: [PATCH] Add rootless mode for PHP 8.3, 8.4 and 8.5 --- .github/workflows/build-containers.yml | 20 +++- 8.2.Dockerfile | 4 +- 8.3.Dockerfile | 27 +++++- 8.4.Dockerfile | 27 +++++- 8.5-edge.Dockerfile | 4 +- 8.5.Dockerfile | 25 ++++- Makefile | 26 ++++- README.md | 60 +++++++++--- run-tests-local.sh | 128 +++++++++++++++++++++---- 9 files changed, 270 insertions(+), 51 deletions(-) diff --git a/.github/workflows/build-containers.yml b/.github/workflows/build-containers.yml index 5580078..4cb307e 100644 --- a/.github/workflows/build-containers.yml +++ b/.github/workflows/build-containers.yml @@ -13,21 +13,30 @@ jobs: strategy: matrix: tag: ["8.2", "8.3", "8.4", "8.5", "8.5-edge"] + mode: ["default", "rootless"] + exclude: + - tag: "8.2" + mode: "rootless" + - tag: "8.5-edge" + mode: "rootless" steps: - name: Checkout the codebase uses: actions/checkout@v6 - name: Build the container for testing - run: make build TAG=$(printenv TAG) + run: make build TAG=$(printenv TAG) MODE=$(printenv MODE) env: TAG: ${{ matrix.tag }} + MODE: ${{ matrix.mode }} - name: Start the container - run: make start TAG=$(printenv TAG) && sleep 10 + run: make start TAG=$(printenv TAG) MODE=$(printenv MODE) && sleep 10 env: TAG: ${{ matrix.tag }} + MODE: ${{ matrix.mode }} - name: Test the container - run: make test TAG=$(printenv TAG) + run: make test TAG=$(printenv TAG) MODE=$(printenv MODE) env: TAG: ${{ matrix.tag }} + MODE: ${{ matrix.mode }} - name: Login to Docker Hub if: github.repository_owner == 'eXistenZNL' && github.ref == 'refs/heads/main' uses: docker/login-action@v3 @@ -35,7 +44,8 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build multi-arch and push the container to Docker Hub + if: github.repository_owner == 'eXistenZNL' && github.ref == 'refs/heads/main' + run: make buildx-and-push TAG=$(printenv TAG) MODE=$(printenv MODE) env: TAG: ${{ matrix.tag }} - if: github.repository_owner == 'eXistenZNL' && github.ref == 'refs/heads/main' - run: make buildx-and-push TAG=$(printenv TAG) + MODE: ${{ matrix.mode }} diff --git a/8.2.Dockerfile b/8.2.Dockerfile index 4196ade..efc8e8b 100644 --- a/8.2.Dockerfile +++ b/8.2.Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.22 +FROM alpine:3.22 AS default LABEL maintainer="docker@stefan-van-essen.nl" @@ -29,7 +29,7 @@ RUN case "${TARGETPLATFORM}" in \ *) \ echo "Cannot build, missing valid build platform." \ exit 1; \ - esac; \ esac; \ + esac; \ rm -rf /tmp/*; COPY files/general files/php82 / diff --git a/8.3.Dockerfile b/8.3.Dockerfile index 2c190ab..22f6952 100644 --- a/8.3.Dockerfile +++ b/8.3.Dockerfile @@ -1,8 +1,8 @@ -FROM alpine:3.22 +FROM alpine:3.22 AS base LABEL maintainer="docker@stefan-van-essen.nl" -ARG S6_OVERLAY_VERSION=3.1.6.2 +ARG S6_OVERLAY_VERSION=3.2.2.0 ARG TARGETPLATFORM # Install webserver packages @@ -44,6 +44,29 @@ WORKDIR /www ENTRYPOINT ["/init"] +# ========================================================= +# Default mode +# ========================================================== + +FROM base AS default + EXPOSE 80 HEALTHCHECK --interval=5s --timeout=5s CMD curl -f http://127.0.0.1/php-fpm-ping || exit 1 + +# ========================================================= +# Rootless mode +# ========================================================= + +FROM default AS rootless + +# Modify configurations and set permissions for rootless operation +RUN sed -i '/^user nginx;/d' /etc/nginx/nginx.conf \ + && sed -i 's|listen 80 default_server;|listen 8080 default_server;|' /etc/nginx/nginx.conf \ + && sed -i '/^user = php$/d; /^group = php$/d' /etc/php83/php-fpm.conf \ + && mkdir -p /var/run/s6 /run/nginx /var/lib/nginx/tmp \ + && chown -R nobody:nobody /var/run/s6 /run /var/lib/nginx /var/log/nginx /www + +EXPOSE 8080 + +HEALTHCHECK --interval=5s --timeout=5s CMD curl -f http://127.0.0.1:8080/php-fpm-ping || exit 1 diff --git a/8.4.Dockerfile b/8.4.Dockerfile index 474ae76..62ad085 100644 --- a/8.4.Dockerfile +++ b/8.4.Dockerfile @@ -1,8 +1,8 @@ -FROM alpine:3.22 +FROM alpine:3.22 AS base LABEL maintainer="docker@stefan-van-essen.nl" -ARG S6_OVERLAY_VERSION=3.1.6.2 +ARG S6_OVERLAY_VERSION=3.2.2.0 ARG TARGETPLATFORM # Install webserver packages @@ -44,6 +44,29 @@ WORKDIR /www ENTRYPOINT ["/init"] +# ========================================================= +# Default mode +# ========================================================== + +FROM base AS default + EXPOSE 80 HEALTHCHECK --interval=5s --timeout=5s CMD curl -f http://127.0.0.1/php-fpm-ping || exit 1 + +# ========================================================= +# Rootless mode +# ========================================================= + +FROM default AS rootless + +# Modify configurations and set permissions for rootless operation +RUN sed -i '/^user nginx;/d' /etc/nginx/nginx.conf \ + && sed -i 's|listen 80 default_server;|listen 8080 default_server;|' /etc/nginx/nginx.conf \ + && sed -i '/^user = php$/d; /^group = php$/d' /etc/php84/php-fpm.conf \ + && mkdir -p /var/run/s6 /run/nginx /var/lib/nginx/tmp \ + && chown -R nobody:nobody /var/run/s6 /run /var/lib/nginx /var/log/nginx /www + +EXPOSE 8080 + +HEALTHCHECK --interval=5s --timeout=5s CMD curl -f http://127.0.0.1:8080/php-fpm-ping || exit 1 diff --git a/8.5-edge.Dockerfile b/8.5-edge.Dockerfile index 3e05c78..16d74fc 100644 --- a/8.5-edge.Dockerfile +++ b/8.5-edge.Dockerfile @@ -1,8 +1,8 @@ -FROM alpine:3.22 +FROM alpine:3.22 AS default LABEL maintainer="docker@stefan-van-essen.nl" -ARG S6_OVERLAY_VERSION=3.1.6.2 +ARG S6_OVERLAY_VERSION=3.2.2.0 ARG TARGETPLATFORM # Install webserver packages diff --git a/8.5.Dockerfile b/8.5.Dockerfile index 1ce325b..cebec71 100644 --- a/8.5.Dockerfile +++ b/8.5.Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.23 +FROM alpine:3.23 AS base LABEL maintainer="docker@stefan-van-essen.nl" @@ -44,6 +44,29 @@ WORKDIR /www ENTRYPOINT ["/init"] +# ========================================================= +# Default mode +# ========================================================== + +FROM base AS default + EXPOSE 80 HEALTHCHECK --interval=5s --timeout=5s CMD curl -f http://127.0.0.1/php-fpm-ping || exit 1 + +# ========================================================= +# Rootless mode +# ========================================================= + +FROM default AS rootless + +# Modify configurations and set permissions for rootless operation +RUN sed -i '/^user nginx;/d' /etc/nginx/nginx.conf \ + && sed -i 's|listen 80 default_server;|listen 8080 default_server;|' /etc/nginx/nginx.conf \ + && sed -i '/^user = php$/d; /^group = php$/d' /etc/php85/php-fpm.conf \ + && mkdir -p /var/run/s6 /run/nginx /var/lib/nginx/tmp \ + && chown -R nobody:nobody /var/run/s6 /run /var/lib/nginx /var/log/nginx /www + +EXPOSE 8080 + +HEALTHCHECK --interval=5s --timeout=5s CMD curl -f http://127.0.0.1:8080/php-fpm-ping || exit 1 diff --git a/Makefile b/Makefile index f6b4d90..48aae05 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ # Variables PROJECTNAME=existenz/webstack TAG=UNDEF +MODE=default PHP_VERSION=$(shell echo "$(TAG)" | sed -e 's/-.*//') .PHONY: all @@ -8,16 +9,29 @@ all: build start test stop clean build: if [ "$(TAG)" = "UNDEF" ]; then echo "Please provide a valid TAG" && exit 1; fi - docker build -t $(PROJECTNAME):$(TAG) --build-arg="BUILDPLATFORM=linux/amd64" -f $(TAG).Dockerfile --pull . + @if [ "$(MODE)" = "default" ]; then \ + docker build -t $(PROJECTNAME):$(TAG) --target $(MODE) --build-arg="BUILDPLATFORM=linux/amd64" -f $(TAG).Dockerfile --pull .; \ + else \ + docker build -t $(PROJECTNAME):$(TAG)-$(MODE) --target $(MODE) --build-arg="BUILDPLATFORM=linux/amd64" -f $(TAG).Dockerfile --pull .; \ + fi buildx-and-push: + if [ "$(TAG)" = "UNDEF" ]; then echo "Please provide a valid TAG" && exit 1; fi docker buildx create --use - docker buildx build --platform=linux/amd64,linux/arm64 -f $(TAG).Dockerfile -t $(PROJECTNAME):$(TAG) . --push + @if [ "$(MODE)" = "default" ]; then \ + docker buildx build --platform=linux/amd64,linux/arm64 --target $(MODE) -f $(TAG).Dockerfile -t $(PROJECTNAME):$(TAG) . --push; \ + else \ + docker buildx build --platform=linux/amd64,linux/arm64 --target $(MODE) -f $(TAG).Dockerfile -t $(PROJECTNAME):$(TAG)-$(MODE) . --push; \ + fi docker buildx stop start: if [ "$(TAG)" = "UNDEF" ]; then echo "please provide a valid TAG" && exit 1; fi - docker run -d -p 8080:80 --name existenz_webstack_instance $(PROJECTNAME):$(TAG) + @if [ "$(MODE)" = "rootless" ]; then \ + docker run -d -p 8080:8080 --user nobody --name existenz_webstack_instance $(PROJECTNAME):$(TAG)-rootless; \ + elif [ "$(MODE)" = "default" ]; then \ + docker run -d -p 8080:80 --name existenz_webstack_instance $(PROJECTNAME):$(TAG); \ + fi stop: docker stop -t0 existenz_webstack_instance || true @@ -26,7 +40,11 @@ stop: clean: if [ "$(TAG)" = "UNDEF" ]; then echo "please provide a valid TAG" && exit 1; fi rm -rf files/s6-overlay || true - docker rmi $(PROJECTNAME):$(TAG) || true + @if [ "$(MODE)" = "default" ]; then \ + docker rmi $(PROJECTNAME):$(TAG) || true; \ + else \ + docker rmi $(PROJECTNAME):$(TAG)-$(MODE) || true; \ + fi test: if [ "$(TAG)" = "UNDEF" ]; then echo "please provide a valid TAG" && exit 1; fi diff --git a/README.md b/README.md index 6372490..0faec5d 100644 --- a/README.md +++ b/README.md @@ -27,14 +27,35 @@ You can create your own containers based upon this container with a simple FROM ### Before you start Before start hacking away, you should know this: -- Nginx runs under the system's nginx user, and PHP-FPM runs under the system's php user. -- The code should be copied into /www, as this is the default directory Nginx and PHP work with in this container. -- When not using a CMS or framework like Laravel / Symfony / WordPress that brings its own public folder, copy to /www/public instead. +- By default, Nginx runs under the system's nginx user, and PHP-FPM runs under the system's php user. For rootless, see below. +- The code should be copied into `/www`, as this is the default directory Nginx and PHP work with in this container. +- When not using a CMS or framework like Laravel / Symfony / WordPress that brings its own public folder, copy to `/www/public` instead. - Any PHP modules needed in your project should be installed by using apk, Alpine Linux's package manager and the package names for installing can be looked up in the version table below. Then there are some tips or rather guidelines that I adhere to personally, but ultimately this is just a matter of taste: - [S6-overlay can set permissions when the container starts up](https://github.com/just-containers/s6-overlay#fixing-ownership--permissions), but this can be slow if a lot of permissions need to be set, so just do this when building the container. +### Rootless mode + +Starting from **PHP 8.3**, this container supports rootless mode for enhanced security. + +#### What is rootless mode? + +In rootless mode, both Nginx and PHP-FPM run as the same non-root user (typically `nobody`). This eliminates the need +for root privileges inside the container, reducing the attack surface. The container is configured to: +- Run on port 8080 instead of port 80 (non-privileged port) +- Remove user directives from Nginx and PHP-FPM configurations +- Set appropriate permissions for directories that need write access + +#### Running rootless containers + +When running rootless containers, you must specify a non-root user for the user id. Permissions to various folders have +been set to the `nobody` user (id `65534`) so it's easiest to go with that. If you want to use a different user id, make +sure to set the correct owner on certain folders when building the container. For specifics, see the rootless target +in any Dockerfile. + +Also the port is no longer on port `80` because that's a privileged port. Instead, the port has been set to `8080`. + ### Basic example Now that we know all that, we can do something like this: @@ -54,25 +75,36 @@ RUN find /www -type d -exec chmod -R 555 {} \; \ ``` And you should now have a working container that runs your PHP project! +Or, when picking a rootless container: + +``` +FROM existenz/webstack:8.5-rootless + +COPY --chown=nobody:nobody src/ /www +``` + ### Versions -> Tags ending with a `-description` install packages from different repositories to keep up with the latest PHP -> versions. These are probably short-lived and will be replaced with their default counterpart as soon as these PHP -> versions make it into the default Alpine repositories. You can use them, just keep in mind you will have to switch -> over to the default container at one point. +> Tags ending with a `-edge` install packages from the edge repository to keep up with the latest PHP versions. These +> are probably short-lived and will be replaced with their default counterpart as soon as these PHP versions make it +> into the default Alpine repositories. You can use them, just keep in mind you will have to switch over to the default +> container at one point. > > Codecasts containers are no longer provided, see [this issue](https://github.com/codecasts/php-alpine/issues/131) for > more information. See the table below to see what versions are currently available: -| Image tag | Based on | PHP Packages from | S6-Overlay | -|-----------|-------------------|-------------------------------------------------------------------------------------------------|------------| -| 8.2 | Alpine Linux 3.22 | [Alpine Linux repo](https://pkgs.alpinelinux.org/packages?name=php82*&branch=v3.22&arch=x86_64) | Version 1 | -| 8.3 | Alpine Linux 3.22 | [Alpine Linux repo](https://pkgs.alpinelinux.org/packages?name=php83*&branch=v3.22&arch=x86_64) | Version 3 | -| 8.4 | Alpine Linux 3.22 | [Alpine Linux repo](https://pkgs.alpinelinux.org/packages?name=php84*&branch=v3.22&arch=x86_64) | Version 3 | -| 8.5 | Alpine Linux 3.23 | [Alpine Linux repo](https://pkgs.alpinelinux.org/packages?name=php85*&branch=v3.23&arch=x86_64) | Version 3 | -| 8.5-edge | Alpine Linux Edge | [Alpine Linux repo](https://pkgs.alpinelinux.org/packages?name=php85*&branch=edge&arch=x86_64) | Version 3 | +| Image tag | Based on | PHP Packages from | S6-Overlay | +|--------------|-------------------|-------------------------------------------------------------------------------------------------|------------| +| 8.2 | Alpine Linux 3.22 | [Alpine Linux repo](https://pkgs.alpinelinux.org/packages?name=php82*&branch=v3.22&arch=x86_64) | Version 1 | +| 8.3 | Alpine Linux 3.22 | [Alpine Linux repo](https://pkgs.alpinelinux.org/packages?name=php83*&branch=v3.22&arch=x86_64) | Version 3 | +| 8.3-rootless | Alpine Linux 3.22 | [Alpine Linux repo](https://pkgs.alpinelinux.org/packages?name=php83*&branch=v3.22&arch=x86_64) | Version 3 | +| 8.4 | Alpine Linux 3.22 | [Alpine Linux repo](https://pkgs.alpinelinux.org/packages?name=php84*&branch=v3.22&arch=x86_64) | Version 3 | +| 8.4-rootless | Alpine Linux 3.22 | [Alpine Linux repo](https://pkgs.alpinelinux.org/packages?name=php84*&branch=v3.22&arch=x86_64) | Version 3 | +| 8.5 | Alpine Linux 3.23 | [Alpine Linux repo](https://pkgs.alpinelinux.org/packages?name=php85*&branch=v3.23&arch=x86_64) | Version 3 | +| 8.5-rootless | Alpine Linux 3.23 | [Alpine Linux repo](https://pkgs.alpinelinux.org/packages?name=php85*&branch=v3.23&arch=x86_64) | Version 3 | +| 8.5-edge | Alpine Linux Edge | [Alpine Linux repo](https://pkgs.alpinelinux.org/packages?name=php85*&branch=edge&arch=x86_64) | Version 3 | ### Overriding or extending the configuration diff --git a/run-tests-local.sh b/run-tests-local.sh index a805198..9c09e1c 100755 --- a/run-tests-local.sh +++ b/run-tests-local.sh @@ -1,24 +1,114 @@ #!/bin/bash -runtest () { +# Color codes +RED="\e[1;31m" +GREEN="\e[1;32m" +RESET="\e[0m" + +runtest-default () { + local tag=$1 + echo "" + echo "========================================" + echo "Testing $tag (default mode)" + echo "========================================" + echo "" + + echo " > Stopping any running containers..." + make stop TAG=$tag > /dev/null 2>&1 + + echo -n " > Building... " + if make build TAG=$tag MODE=default > /dev/null 2>&1; then + echo -e "${GREEN}OK${RESET}" + else + echo -e "${RED}FAILURE${RESET}" + return 1 + fi + + echo -n " > Starting... " + if make start TAG=$tag MODE=default > /dev/null 2>&1; then + echo -e "${GREEN}OK${RESET}" + else + echo -e "${RED}FAILURE${RESET}" + return 1 + fi + + echo -n " > Testing... " + if make test TAG=$tag MODE=default > /dev/null 2>&1; then + echo -e "${GREEN}OK${RESET}" + else + echo -e "${RED}FAILURE${RESET}" + make stop TAG=$tag > /dev/null 2>&1 + return 1 + fi + + echo -n " > Stopping... " + make stop TAG=$tag > /dev/null 2>&1 + echo -e "${GREEN}OK${RESET}" + + echo "" + echo -e "${GREEN}✓ Default mode passed for $tag${RESET}" + return 0 +} + +runtest-rootless () { + local tag=$1 echo "" - echo "Testing tag $1" - echo " > Stopping any running containers.. " - make stop TAG=$1 > /dev/null 2>&1 - echo -n " > Building... " - make build TAG=$1 > /dev/null 2>&1 - [[ $? == 0 ]] && echo -e "\e[1;32mOK\e[0m" || echo -e "\e[1;31mFAILURE\e[0m" - echo " > Starting... " - make start TAG=$1 > /dev/null 2>&1 - echo -n " > Testing... " - make test TAG=$1 > /dev/null 2>&1 - [[ $? == 0 ]] && echo -e "\e[1;32mOK\e[0m" || echo -e "\e[1;31mFAILURE\e[0m" - echo " > Stopping... " - make stop TAG=$1 > /dev/null 2>&1 + echo "========================================" + echo "Testing $tag (rootless mode)" + echo "========================================" + echo "" + + echo " > Stopping any running containers..." + make stop TAG=$tag > /dev/null 2>&1 + + echo -n " > Building... " + if make build TAG=$tag MODE=rootless > /dev/null 2>&1; then + echo -e "${GREEN}OK${RESET}" + else + echo -e "${RED}FAILURE${RESET}" + return 1 + fi + + echo -n " > Starting... " + if make start TAG=$tag MODE=rootless > /dev/null 2>&1; then + echo -e "${GREEN}OK${RESET}" + else + echo -e "${RED}FAILURE${RESET}" + return 1 + fi + + echo -n " > Testing... " + if make test TAG=$tag MODE=rootless > /dev/null 2>&1; then + echo -e "${GREEN}OK${RESET}" + else + echo -e "${RED}FAILURE${RESET}" + make stop TAG=$tag > /dev/null 2>&1 + return 1 + fi + + echo -n " > Stopping... " + make stop TAG=$tag > /dev/null 2>&1 + echo -e "${GREEN}OK${RESET}" + + echo "" + echo -e "${GREEN}✓ Rootless mode passed for $tag${RESET}" + return 0 } -#runtest "8.2" -#runtest "8.3" -#runtest "8.4" -runtest "8.5" -runtest "8.5-edge" +# PHP 8.2 - default only +runtest-default "8.2" + +# PHP 8.3 - both modes +runtest-default "8.3" +runtest-rootless "8.3" + +# PHP 8.4 - both modes +runtest-default "8.4" +runtest-rootless "8.4" + +# PHP 8.5 - both modes +runtest-default "8.5" +runtest-rootless "8.5" + +# PHP 8.5-edge - default only +runtest-default "8.5-edge"