Users and permissions
Non-root by default
Every image runs as appuser, UID 1000 and GID 1000, unless you pass --user. In this mode:
- Ports below 1024 cannot be bound. Use
NGINX_PORT=8080andNGINX_SSL_PORT=8443. - Cron is not available. The entrypoint starts Nginx and PHP-FPM directly and supervises them itself.
- PHP-FPM runs its workers as
appusertoo; theuserandgrouppool directives are stripped automatically. - Nothing in
/etcis writable. Configuration is rendered into/run/nginxand/run/php.
Root mode
docker run --rm -d -p 80:80 --user root adamrollogi/php-nginx:8.4
Supervisor becomes PID 1's child and manages cron, Nginx and PHP-FPM. Nginx workers run as nginx, FPM workers as nobody. Use this when you need privileged ports or cron jobs; drop files into /etc/cron.d/ or mount a crontab.
File ownership on bind mounts
PHP writes as UID 1000. If your host files are owned by another user, either run with a matching UID or give the mounted directories the right ownership:
docker run --rm -d -p 8080:8080 -e NGINX_PORT=8080 \
--user "$(id -u):$(id -g)" \
-v $(pwd)/public:/var/www/html \
adamrollogi/php-nginx:8.4
Running as an arbitrary UID works because the runtime directories (/run/nginx, /run/php, /var/log, /var/lib/nginx) are group-writable by GID 1000 and the entrypoint creates what it needs with best-effort mkdir. If you pick a UID outside that group, mount those paths as tmpfs with matching ownership, as in the read-only recipe below.
Read-only root filesystem
Because configuration never lands in /etc, the web images run cleanly with --read-only. Any tampering with rendered config or logs is discarded on restart, since tmpfs mounts are recreated.
docker run --rm -d -p 8080:8080 \
-e NGINX_PORT=8080 \
--read-only \
--tmpfs /run:uid=1000,gid=1000 \
--tmpfs /var/log:uid=1000,gid=1000 \
--tmpfs /var/lib/nginx:uid=1000,gid=1000 \
--tmpfs /tmp \
--cap-drop ALL --security-opt no-new-privileges \
-v $(pwd)/public:/var/www/html:ro \
adamrollogi/php-nginx:8.4
The uid/gid options matter: Docker mounts tmpfs as root-owned by default. Add --tmpfs /etc/nginx/ssl:uid=1000,gid=1000 if you use NGINX_SSL_SELF_SIGNED=true.
In Compose:
services:
app:
image: adamrollogi/php-nginx:8.4
read_only: true
tmpfs:
- /run:uid=1000,gid=1000
- /var/log:uid=1000,gid=1000
- /var/lib/nginx:uid=1000,gid=1000
- /tmp
cap_drop: [ALL]
security_opt: [no-new-privileges:true]
Why config lives in /run
In non-root mode the user that renders configuration is the same user that executes PHP, so a file-write vulnerability in an application could in principle rewrite php.ini and gain persistence. Keeping /etc root-owned and rendering into /run means the writable surface is a tmpfs-able directory that resets on restart, and the read-only recipe above removes persistence entirely. It is a mitigation, not a sandbox; treat application-level write bugs as the real problem.
No setuid binaries
find / -perm /6000 returns nothing in any image. busybox-suid is not installed and the setuid bits from the shadow package are stripped at build time, so there is no su, passwd or crontab privilege escalation path for a compromised process.