From 1ff8c81f4be6f84865618c8558f816ecbac0dfca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20L=C3=BCtzner?= Date: Sun, 4 Oct 2026 10:06:02 +0200 Subject: [PATCH] Let the image run as a user that is not root Nothing here needs root at run time, and a platform that chooses the uid itself and drops every capability could not start it. Three things were in the way, found by running it as uid 568 with nothing granted: frankenphp carried cap_net_bind_service as a file capability, and a process without that capability may not execute such a binary at all. Removed: in a container the unprivileged port range starts at 0 anyway. var/ cache, log and share were root's. Writable by anyone now, since the uid is not known when the image is built. /data, /config root's as well, so the hub could not open its database on a fresh volume. Same answer. The entrypoint's setfacl step is skipped for a non-root user, who can grant nothing and has no name for whoami to print. Installs that run as root, which is every compose file here, are unchanged. Co-Authored-By: Claude Opus 5.5 --- Dockerfile | 32 +++++++++++++++++++++++++++++++- frankenphp/docker-entrypoint.sh | 15 +++++++++++---- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index b00f9702..5ab2e996 100644 --- a/Dockerfile +++ b/Dockerfile @@ -100,6 +100,28 @@ COPY --link frankenphp/Caddyfile /etc/frankenphp/Caddyfile COPY --link --from=mercure_upstream /usr/bin/caddy /usr/local/bin/mercure COPY --link --from=mercure_upstream /etc/caddy/Caddyfile /etc/mercure/Caddyfile +# Runnable as a user that is not root, and not any particular one. +# +# Nothing here needs root at run time, and a platform that picks the uid itself +# (the TrueNAS catalogue runs apps as 568 by default, with every capability +# dropped) found two things in the way before PHP ever started: +# +# - The FrankenPHP binary carries cap_net_bind_service as a FILE capability. +# A process that does not hold that capability may not execute such a file +# at all: "exec: frankenphp: Operation not permitted". It is there so a +# non-root user can bind port 80 on a bare host; in a container the +# unprivileged port range starts at 0, so it buys nothing and costs the +# start. Root keeps binding 80 exactly as before. +# - /data and /config, where the web server and the hub keep their state, +# belong to root. A new volume mounted there copies the directory's +# permissions, so the hub died on "open /data/caddy/mercure.db: permission +# denied". Writable by anyone, because the uid is not known here; there is +# one tenant in this container. +RUN set -eux; \ + setcap -r /usr/local/bin/frankenphp; \ + mkdir -p /data/caddy /config/caddy; \ + chmod -R a+rwX /data /config + ENTRYPOINT ["docker-entrypoint"] # /healthz, not Caddy's metrics port. The old probe answered as soon as the web @@ -195,4 +217,12 @@ RUN set -eux; \ composer run-script --no-dev post-install-cmd; \ php bin/console tailwind:build --minify; \ php bin/console asset-map:compile; \ - chmod +x bin/console; sync; + chmod +x bin/console; \ + # The directories PHP writes at run time, for whichever user that turns out + # to be — see the note on running without root in the base stage. var/ itself + # as well, not recursively: the default secrets directory is created in it + # on first start. + mkdir -p var/share; \ + chmod a+rwx var; \ + chmod -R a+rwX var/cache var/log var/share; \ + sync; diff --git a/frankenphp/docker-entrypoint.sh b/frankenphp/docker-entrypoint.sh index 2f91fa16..cad1e4d4 100644 --- a/frankenphp/docker-entrypoint.sh +++ b/frankenphp/docker-entrypoint.sh @@ -257,11 +257,18 @@ if [ "$1" = 'frankenphp' ] || [ "$1" = 'php' ] || [ "$1" = 'bin/console' ]; then # TrueNAS layout set off on it at once. mkdir -p var/cache var/log - if ! setfacl -R -m u:www-data:rwX -m u:"$(whoami)":rwX var/cache var/log 2>/dev/null; then - echo 'Note: POSIX ACLs are not supported on this filesystem; skipping setfacl for var/cache and var/log.' - fi + # Root only. As any other user there is nobody to grant anything to — the + # process can only change ACLs on what it owns, and a uid a platform picked + # has no name for `whoami` to answer with ("cannot find name for user ID + # 568", twice, on every start). The image makes both directories writable + # for that case; see the Dockerfile. + if [ "$(id -u)" = '0' ]; then + if ! setfacl -R -m u:www-data:rwX -m u:"$(whoami)":rwX var/cache var/log 2>/dev/null; then + echo 'Note: POSIX ACLs are not supported on this filesystem; skipping setfacl for var/cache and var/log.' + fi - setfacl -dR -m u:www-data:rwX -m u:"$(whoami)":rwX var/cache var/log 2>/dev/null || true + setfacl -dR -m u:www-data:rwX -m u:"$(whoami)":rwX var/cache var/log 2>/dev/null || true + fi fi exec docker-php-entrypoint "$@"