From adb526bb60ddf1c54f3b481b5eac9f841cf15cdc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 30 Sep 2026 09:39:16 +0000 Subject: [PATCH 1/3] Apply remaining changes Co-authored-by: bdossantos <245284+bdossantos@users.noreply.github.com> --- .github/actions/docker/action.yml | 14 ++++++++++++-- .github/workflows/cd.yml | 21 +++++++++++++++++++++ README.md | 2 +- docker-compose.ci.yml | 3 +++ dockerfiles/php-lol/Dockerfile.8.3 | 20 ++++++++++++++++++++ dockerfiles/php-lol/Dockerfile.8.4 | 20 ++++++++++++++++++++ dockerfiles/php-lol/Dockerfile.8.5 | 20 ++++++++++++++++++++ 7 files changed, 97 insertions(+), 3 deletions(-) diff --git a/.github/actions/docker/action.yml b/.github/actions/docker/action.yml index e80e8aea..63e500e3 100644 --- a/.github/actions/docker/action.yml +++ b/.github/actions/docker/action.yml @@ -8,6 +8,14 @@ inputs: registry_password: description: "Password for image registry" required: true + target: + description: "Dockerfile build target" + required: false + default: "run" + tag_suffix: + description: "Suffix appended to every image tag (e.g. -rootless)" + required: false + default: "" runs: using: "composite" @@ -31,8 +39,9 @@ runs: version=$(grep -E '^\s*(ENV\s+)?VERSION=' '${{ inputs.dockerfile }}' | tail -1 | sed -E 's/^.*VERSION=([^ \\]+).*/\1/') vcs_ref=$(git rev-parse --short HEAD) image_name=$(cd $(dirname ${{ inputs.dockerfile }}) && echo "${PWD##*/}") - tags=bdossantos/${image_name}:latest,bdossantos/${image_name}:${version},bdossantos/${image_name}:${version}-${vcs_ref},bdossantos/${image_name}:${version%.*} - cache_image=bdossantos/${image_name}:buildcache + suffix='${{ inputs.tag_suffix }}' + tags=bdossantos/${image_name}:latest${suffix},bdossantos/${image_name}:${version}${suffix},bdossantos/${image_name}:${version}-${vcs_ref}${suffix},bdossantos/${image_name}:${version%.*}${suffix} + cache_image=bdossantos/${image_name}:buildcache${suffix} echo "build_date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT echo "cache_image=${cache_image}" >> $GITHUB_OUTPUT echo "path=$(dirname ${{ inputs.dockerfile }})" >> $GITHUB_OUTPUT @@ -49,6 +58,7 @@ runs: cache-to: type=registry,ref=${{ steps.context.outputs.cache_image }},mode=max context: ${{ steps.context.outputs.path }} file: ${{ inputs.dockerfile }} + target: ${{ inputs.target }} push: ${{ github.event_name != 'pull_request' }} sbom: true tags: ${{ steps.context.outputs.tags }} diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 74cd1948..8ef79513 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -16,13 +16,16 @@ jobs: contents: read outputs: dockerfiles: ${{ steps.discover.outputs.dockerfiles }} + rootless: ${{ steps.discover.outputs.rootless }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # ratchet:actions/checkout@v7.0.1 - name: Discover Dockerfiles id: discover run: | dockerfiles=$(find dockerfiles -type f -name 'Dockerfile*' | sort | jq -R -s -c 'split("\n")[:-1]') + rootless=$(find dockerfiles/php-lol -type f -name 'Dockerfile*' | sort | jq -R -s -c 'split("\n")[:-1]') echo "dockerfiles=${dockerfiles}" >> $GITHUB_OUTPUT + echo "rootless=${rootless}" >> $GITHUB_OUTPUT build: runs-on: ubuntu-latest @@ -39,3 +42,21 @@ jobs: with: dockerfile: ${{ matrix.dockerfile }} registry_password: ${{ secrets.DOCKERHUB_TOKEN }} + + build-rootless: + runs-on: ubuntu-latest + permissions: + contents: read + needs: discover + strategy: + matrix: + dockerfile: ${{ fromJSON(needs.discover.outputs.rootless) }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # ratchet:actions/checkout@v7.0.1 + - name: Build and push rootless variant + uses: ./.github/actions/docker + with: + dockerfile: ${{ matrix.dockerfile }} + registry_password: ${{ secrets.DOCKERHUB_TOKEN }} + target: rootless + tag_suffix: -rootless diff --git a/README.md b/README.md index 46f8ff82..4813cb52 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ multi-stage builds, pinned dependencies, and non-root users. | [dnscrypt-proxy](dockerfiles/dnscrypt-proxy) | [DNSCrypt](https://dnscrypt.info/) proxy with encrypted DNS support | | [memcached](dockerfiles/memcached) | [Memcached](https://memcached.org/) with built-in proxy support and embedded [routelib](https://github.com/memcached/memcached-proxylibs) | | [paperless-ngx](dockerfiles/paperless-ngx) | [Paperless-ngx](https://docs.paperless-ngx.com/) document management system | -| [php-lol](dockerfiles/php-lol) | PHP-FPM with Nginx and common extensions (8.3, 8.4, 8.5) | +| [php-lol](dockerfiles/php-lol) | PHP-FPM with Nginx and common extensions (8.3, 8.4, 8.5); `-rootless` tags run as 65534 on port 8080 | | [pingdom-exporter](dockerfiles/pingdom-exporter) | Prometheus exporter for Pingdom metrics | | [pint](dockerfiles/pint) | [Pint](https://cloudflare.github.io/pint/) – Prometheus rule linter by Cloudflare | | [python-github-backup](dockerfiles/python-github-backup) | [python-github-backup](https://github.com/josegonzalez/python-github-backup) – GitHub repository backup tool | diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml index bef51ba7..b53a94d6 100644 --- a/docker-compose.ci.yml +++ b/docker-compose.ci.yml @@ -21,6 +21,7 @@ services: build: context: dockerfiles/php-lol dockerfile: Dockerfile.8.3 + target: run image: bdossantos/php-lol container_name: php-lol83 php-lol84: @@ -28,6 +29,7 @@ services: build: context: dockerfiles/php-lol dockerfile: Dockerfile.8.4 + target: run image: bdossantos/php-lol container_name: php-lol84 php-lol85: @@ -35,6 +37,7 @@ services: build: context: dockerfiles/php-lol dockerfile: Dockerfile.8.5 + target: run image: bdossantos/php-lol container_name: php-lol85 pingdom-exporter: diff --git a/dockerfiles/php-lol/Dockerfile.8.3 b/dockerfiles/php-lol/Dockerfile.8.3 index 7abd3fe9..7446afb7 100644 --- a/dockerfiles/php-lol/Dockerfile.8.3 +++ b/dockerfiles/php-lol/Dockerfile.8.3 @@ -254,3 +254,23 @@ EXPOSE 80/tcp 443/tcp ENTRYPOINT ["/entrypoint.sh"] CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"] + +# +# Rootless +# +# Same as "run", but the whole process tree (supervisord, php-fpm, nginx) runs +# as 65534:65534 and nginx listens on unprivileged port 8080. +# +FROM run AS rootless + +RUN set -eux \ + && sed -i -e '/^user /d' -e 's/listen \[::\]:80 /listen [::]:8080 /' -e 's/listen 80 /listen 8080 /' /etc/nginx/nginx.conf \ + && sed -i -e '/^chown = /d' -e '/^user = /d' /etc/supervisor/supervisord.conf \ + && sed -i -e '/^user = /d' /etc/supervisor/conf.d/app.conf \ + && chmod 0444 /etc/nginx/nginx.conf /etc/supervisor/supervisord.conf /etc/supervisor/conf.d/app.conf \ + && mkdir -p /var/cache/nginx /var/log/nginx /var/lib/php/session \ + && chown -R 65534:65534 /var/cache/nginx /var/log/nginx /var/lib/php/session + +EXPOSE 8080/tcp + +USER 65534:65534 diff --git a/dockerfiles/php-lol/Dockerfile.8.4 b/dockerfiles/php-lol/Dockerfile.8.4 index 36621d2e..09ad7455 100644 --- a/dockerfiles/php-lol/Dockerfile.8.4 +++ b/dockerfiles/php-lol/Dockerfile.8.4 @@ -252,3 +252,23 @@ EXPOSE 80/tcp 443/tcp ENTRYPOINT ["/entrypoint.sh"] CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"] + +# +# Rootless +# +# Same as "run", but the whole process tree (supervisord, php-fpm, nginx) runs +# as 65534:65534 and nginx listens on unprivileged port 8080. +# +FROM run AS rootless + +RUN set -eux \ + && sed -i -e '/^user /d' -e 's/listen \[::\]:80 /listen [::]:8080 /' -e 's/listen 80 /listen 8080 /' /etc/nginx/nginx.conf \ + && sed -i -e '/^chown = /d' -e '/^user = /d' /etc/supervisor/supervisord.conf \ + && sed -i -e '/^user = /d' /etc/supervisor/conf.d/app.conf \ + && chmod 0444 /etc/nginx/nginx.conf /etc/supervisor/supervisord.conf /etc/supervisor/conf.d/app.conf \ + && mkdir -p /var/cache/nginx /var/log/nginx /var/lib/php/session \ + && chown -R 65534:65534 /var/cache/nginx /var/log/nginx /var/lib/php/session + +EXPOSE 8080/tcp + +USER 65534:65534 diff --git a/dockerfiles/php-lol/Dockerfile.8.5 b/dockerfiles/php-lol/Dockerfile.8.5 index 5a3b1499..1af240e3 100644 --- a/dockerfiles/php-lol/Dockerfile.8.5 +++ b/dockerfiles/php-lol/Dockerfile.8.5 @@ -318,3 +318,23 @@ EXPOSE 80/tcp 443/tcp ENTRYPOINT ["/entrypoint.sh"] CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"] + +# +# Rootless +# +# Same as "run", but the whole process tree (supervisord, php-fpm, nginx) runs +# as 65534:65534 and nginx listens on unprivileged port 8080. +# +FROM run AS rootless + +RUN set -eux \ + && sed -i -e '/^user /d' -e 's/listen \[::\]:80 /listen [::]:8080 /' -e 's/listen 80 /listen 8080 /' /etc/nginx/nginx.conf \ + && sed -i -e '/^chown = /d' -e '/^user = /d' /etc/supervisor/supervisord.conf \ + && sed -i -e '/^user = /d' /etc/supervisor/conf.d/app.conf \ + && chmod 0444 /etc/nginx/nginx.conf /etc/supervisor/supervisord.conf /etc/supervisor/conf.d/app.conf \ + && mkdir -p /var/cache/nginx /var/log/nginx /var/lib/php/session \ + && chown -R 65534:65534 /var/cache/nginx /var/log/nginx /var/lib/php/session + +EXPOSE 8080/tcp + +USER 65534:65534 From c6d5a7f2114a4d64aff1ed4ddc6a122910916eec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 30 Sep 2026 09:57:19 +0000 Subject: [PATCH 2/3] Apply remaining changes Co-authored-by: bdossantos <245284+bdossantos@users.noreply.github.com> --- .github/actions/docker/action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/docker/action.yml b/.github/actions/docker/action.yml index 63e500e3..1d5615c5 100644 --- a/.github/actions/docker/action.yml +++ b/.github/actions/docker/action.yml @@ -9,9 +9,9 @@ inputs: description: "Password for image registry" required: true target: - description: "Dockerfile build target" + description: "Dockerfile build target (defaults to the last stage)" required: false - default: "run" + default: "" tag_suffix: description: "Suffix appended to every image tag (e.g. -rootless)" required: false From e5bcb701e2397bf91cee5d8ad0a9544c81489fd8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 30 Sep 2026 12:06:37 +0000 Subject: [PATCH 3/3] fix(php-lol): keep run as final stage, test rootless in CI, single latest-rootless Co-authored-by: bdossantos <245284+bdossantos@users.noreply.github.com> --- .github/actions/docker/action.yml | 11 ++++++++++- .github/workflows/cd.yml | 1 + .github/workflows/ci.yml | 19 +++++++++++++++++++ dockerfiles/php-lol/Dockerfile.8.3 | 28 ++++++++++++++++++++-------- dockerfiles/php-lol/Dockerfile.8.4 | 28 ++++++++++++++++++++-------- dockerfiles/php-lol/Dockerfile.8.5 | 28 ++++++++++++++++++++-------- 6 files changed, 90 insertions(+), 25 deletions(-) diff --git a/.github/actions/docker/action.yml b/.github/actions/docker/action.yml index 1d5615c5..716b0bee 100644 --- a/.github/actions/docker/action.yml +++ b/.github/actions/docker/action.yml @@ -17,6 +17,11 @@ inputs: required: false default: "" + tag_latest: + description: "Whether to also publish the latest tag" + required: false + default: "true" + runs: using: "composite" steps: @@ -40,7 +45,11 @@ runs: vcs_ref=$(git rev-parse --short HEAD) image_name=$(cd $(dirname ${{ inputs.dockerfile }}) && echo "${PWD##*/}") suffix='${{ inputs.tag_suffix }}' - tags=bdossantos/${image_name}:latest${suffix},bdossantos/${image_name}:${version}${suffix},bdossantos/${image_name}:${version}-${vcs_ref}${suffix},bdossantos/${image_name}:${version%.*}${suffix} + tags="" + if [ '${{ inputs.tag_latest }}' = 'true' ]; then + tags=bdossantos/${image_name}:latest${suffix}, + fi + tags=${tags}bdossantos/${image_name}:${version}${suffix},bdossantos/${image_name}:${version}-${vcs_ref}${suffix},bdossantos/${image_name}:${version%.*}${suffix} cache_image=bdossantos/${image_name}:buildcache${suffix} echo "build_date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT echo "cache_image=${cache_image}" >> $GITHUB_OUTPUT diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 8ef79513..e5efe1e6 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -60,3 +60,4 @@ jobs: registry_password: ${{ secrets.DOCKERHUB_TOKEN }} target: rootless tag_suffix: -rootless + tag_latest: ${{ matrix.dockerfile == 'dockerfiles/php-lol/Dockerfile.8.5' }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c28d7866..46a0033a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,3 +69,22 @@ jobs: - name: Lint and test run: | make test + - name: Test rootless php-lol variants + run: | + for version in 8.3 8.4 8.5; do + image="bdossantos/php-lol:rootless-${version}" + docker build --target rootless -f "dockerfiles/php-lol/Dockerfile.${version}" -t "${image}" dockerfiles/php-lol + test "$(docker image inspect --format '{{.Config.User}}' "${image}")" = "65534:65534" + docker run -d --name "php-lol-rootless-${version}" --read-only --tmpfs /tmp --tmpfs /run --tmpfs /var/log --tmpfs /var/lib/php/session --tmpfs /var/cache/nginx "${image}" + ok=false + for _ in $(seq 1 30); do + if docker exec "php-lol-rootless-${version}" curl -sf http://127.0.0.1:8080/nginx_status >/dev/null; then + ok=true + break + fi + sleep 1 + done + docker logs "php-lol-rootless-${version}" + docker rm -f "php-lol-rootless-${version}" + test "${ok}" = "true" + done diff --git a/dockerfiles/php-lol/Dockerfile.8.3 b/dockerfiles/php-lol/Dockerfile.8.3 index 7446afb7..6fb6d0e7 100644 --- a/dockerfiles/php-lol/Dockerfile.8.3 +++ b/dockerfiles/php-lol/Dockerfile.8.3 @@ -222,9 +222,11 @@ RUN chmod 0444 \ /usr/local/etc/php/php.ini # -# Run +# Runtime +# +# Shared by the "rootless" and "run" stages, ports are declared per stage. # -FROM foundation AS run +FROM foundation AS runtime ARG BUILD_DATE ARG VCS_REF @@ -250,18 +252,13 @@ LABEL org.label-schema.build-date="$BUILD_DATE" \ WORKDIR /usr/src/app -EXPOSE 80/tcp 443/tcp - -ENTRYPOINT ["/entrypoint.sh"] -CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"] - # # Rootless # # Same as "run", but the whole process tree (supervisord, php-fpm, nginx) runs # as 65534:65534 and nginx listens on unprivileged port 8080. # -FROM run AS rootless +FROM runtime AS rootless RUN set -eux \ && sed -i -e '/^user /d' -e 's/listen \[::\]:80 /listen [::]:8080 /' -e 's/listen 80 /listen 8080 /' /etc/nginx/nginx.conf \ @@ -274,3 +271,18 @@ RUN set -eux \ EXPOSE 8080/tcp USER 65534:65534 + +ENTRYPOINT ["/entrypoint.sh"] +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"] + +# +# Run +# +# Last stage: plain "docker build" keeps producing the port 80 image. +# +FROM runtime AS run + +EXPOSE 80/tcp 443/tcp + +ENTRYPOINT ["/entrypoint.sh"] +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"] diff --git a/dockerfiles/php-lol/Dockerfile.8.4 b/dockerfiles/php-lol/Dockerfile.8.4 index 09ad7455..5444b505 100644 --- a/dockerfiles/php-lol/Dockerfile.8.4 +++ b/dockerfiles/php-lol/Dockerfile.8.4 @@ -220,9 +220,11 @@ RUN chmod 0444 \ /usr/local/etc/php/php.ini # -# Run +# Runtime +# +# Shared by the "rootless" and "run" stages, ports are declared per stage. # -FROM foundation AS run +FROM foundation AS runtime ARG BUILD_DATE ARG VCS_REF @@ -248,18 +250,13 @@ LABEL org.label-schema.build-date="$BUILD_DATE" \ WORKDIR /usr/src/app -EXPOSE 80/tcp 443/tcp - -ENTRYPOINT ["/entrypoint.sh"] -CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"] - # # Rootless # # Same as "run", but the whole process tree (supervisord, php-fpm, nginx) runs # as 65534:65534 and nginx listens on unprivileged port 8080. # -FROM run AS rootless +FROM runtime AS rootless RUN set -eux \ && sed -i -e '/^user /d' -e 's/listen \[::\]:80 /listen [::]:8080 /' -e 's/listen 80 /listen 8080 /' /etc/nginx/nginx.conf \ @@ -272,3 +269,18 @@ RUN set -eux \ EXPOSE 8080/tcp USER 65534:65534 + +ENTRYPOINT ["/entrypoint.sh"] +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"] + +# +# Run +# +# Last stage: plain "docker build" keeps producing the port 80 image. +# +FROM runtime AS run + +EXPOSE 80/tcp 443/tcp + +ENTRYPOINT ["/entrypoint.sh"] +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"] diff --git a/dockerfiles/php-lol/Dockerfile.8.5 b/dockerfiles/php-lol/Dockerfile.8.5 index 1af240e3..6a4c27ff 100644 --- a/dockerfiles/php-lol/Dockerfile.8.5 +++ b/dockerfiles/php-lol/Dockerfile.8.5 @@ -286,9 +286,11 @@ RUN chmod 0444 \ /usr/local/etc/php/php.ini # -# Run +# Runtime +# +# Shared by the "rootless" and "run" stages, ports are declared per stage. # -FROM foundation AS run +FROM foundation AS runtime ARG BUILD_DATE ARG VCS_REF @@ -314,18 +316,13 @@ LABEL org.label-schema.build-date="$BUILD_DATE" \ WORKDIR /usr/src/app -EXPOSE 80/tcp 443/tcp - -ENTRYPOINT ["/entrypoint.sh"] -CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"] - # # Rootless # # Same as "run", but the whole process tree (supervisord, php-fpm, nginx) runs # as 65534:65534 and nginx listens on unprivileged port 8080. # -FROM run AS rootless +FROM runtime AS rootless RUN set -eux \ && sed -i -e '/^user /d' -e 's/listen \[::\]:80 /listen [::]:8080 /' -e 's/listen 80 /listen 8080 /' /etc/nginx/nginx.conf \ @@ -338,3 +335,18 @@ RUN set -eux \ EXPOSE 8080/tcp USER 65534:65534 + +ENTRYPOINT ["/entrypoint.sh"] +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"] + +# +# Run +# +# Last stage: plain "docker build" keeps producing the port 80 image. +# +FROM runtime AS run + +EXPOSE 80/tcp 443/tcp + +ENTRYPOINT ["/entrypoint.sh"] +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]