auvem/php-fpm-wordpress — multi-version PHP-FPM + nginx Docker images
This repository contains Dockerfiles and configuration for building PHP-FPM images optimized for WordPress and a companion nginx image. It aims to make adding and maintaining multiple PHP versions straightforward while keeping builds reproducible and small.
Repository Layout
php-fpm/— top-level directory containing PHP version lanes7.4/— PHP 7.4 FPM lane (multi-stage, Alpine-based)Dockerfile— builds PHP + required extensions
...
nginx/— NGINX build configuration with batteries-included configuration fileDockerfile— nginx:alpine-slim image that shipsnginx.confnginx.conf— default server config that works with the php-fpm image
shared/php-fpm/— canonical shared fileswww.conf— canonical php-fpm pool configentrypoint.sh— optional guarded entrypoint to fix mount permissions at container start
Note about shared files and builds
The CI workflow is configured to build with the repository root as the Docker build context and to point Docker to lane Dockerfiles (for example, file: docker/7.4/Dockerfile). That means Dockerfiles can safely COPY shared files from docker/php-fpm/ without requiring per-lane duplicates. This reduces maintenance overhead — keep the canonical copy in docker/php-fpm/www.conf and the CI will make it available to all lanes.
Adding a new PHP version
- Create
php-fpm/<version>/(e.g.php-fpm/8.1/). - Copy
php-fpm/7.4/Dockerfile(for example) into the new directory and updateARG BASE_TAGto the desiredphp:<version>-fpm-alpinetag. - Adjust
docker-php-ext-install/build deps if needed. - Update the
matrix.lanelist for thebuildjob in.github/workflows/php-fpm.yml. - Push — CI will detect the new lane and build it. NOTE: CI will additionally rebuild all other images registered in
matrix.lane, cost tradeoff of this overhead vs. the benefits of intermittent rebuilds with latest OS security patches built-in should be weighed.
Bind mounts and permissions
-
Images use
/var/www/htmlas the webroot. When you mount a host directory over that path the mount replaces the image contents, including ownership. -
Recommended safe options:
- Pre-chown host files to UID/GID 1000 before starting containers:
sudo chown -R 1000:1000 ./wp_root- Or enable the entrypoint-based fixup in php-fpm by setting
CHOWN_ON_START=1for thephp-fpmservice (the entrypoint is guarded — it only runs when this env is explicitly enabled).
Logging & debugging
- PHP-FPM streams master/worker logs plus PHP fatals to stderr, so
docker compose logs php-fpm(or your platform equivalent) will always contain the messages you need for incident response. - The auto-prepend bootstrap additionally installs shutdown/exception hooks that write uncaught throwables and fatal errors to stderr even if WordPress or a plugin tampers with
ini_set(). - When you need full stack traces in the browser, set
FORCE_DEBUG_ERRORS=1on thephp-fpmservice. The bootstrap enables verbose output and emits a warning in the container logs reminding you to turn it back off (leaving it on in production leaks stack traces to clients). - Remove or unset
FORCE_DEBUG_ERRORSafter troubleshooting so production responses stay clean.
Health checks
- The nginx config exposes
/healthz, which bypasses WordPress entirely and executes/usr/local/share/auvem/health/healthcheck.phpinside PHP-FPM. The script verifies the/var/www/htmlmount and optionally performs a MySQL connection test whenWORDPRESS_DB_*variables are present. - HTTP 200 means the platform (nginx ↔ php-fpm ↔ database) is healthy. Any filesystem or database failures return HTTP 503 with a short JSON payload detailing which probe failed. This lets you point uptime monitors at
/healthzwhile still separately watching the public homepage for regressions caused by client code. - Example local probe:
curl -fsS http://localhost:8080/healthz | jq.
WP-CLI baked in
- The PHP-FPM image now ships with the official
wpbinary, so you can run administrative remediation tasks without installing extra tooling on target hosts. - Typical usage:
docker compose exec php-fpm wp --allow-root user update admin --user_pass='NewPassword123!' --skip-email --path=/var/www/html. - The binary runs as root inside the container, so remember the
--allow-rootflag (or create an app-level user if you prefer tighter isolation). - Automation hooks (e.g., your client portal) can wrap
wpcommands for password resets, cron runs, plugin inspections, etc., provided they bind-mount the site into/var/www/html.
Local Testing & Development
Use the provided docker-compose.yml in the repo root for local development — it builds images from the repo (so shared files are available) and mounts ./wp_root for site content.
Production Example
Below is an example docker-compose.yml for production deployments that pulls images from the package registry at gitea.auvem.com. Adjust image versions and secrets as appropriate.
services:
mariadb:
image: mariadb:latest
restart: unless-stopped
environment:
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
volumes:
- ./db_data:/var/lib/mysql
php-fpm:
image: gitea.auvem.com/auvem/wordpress-docker/php-fpm:7.4-stable
restart: unless-stopped
depends_on:
- mariadb
volumes:
- ./wp_root:/var/www/html:rw
nginx:
image: gitea.auvem.com/auvem/wordpress-docker/nginx:latest
restart: unless-stopped
ports:
- "80:80"
depends_on:
- php-fpm
volumes:
- ./wp_root:/var/www/html:ro
CI / build notes
Security & hardening
- Multi-stage builds keep final images minimal and reduce attack surface.
- PHP uses
php.ini-productionwith opcache tuned. The php-fpm pool is configured to drop workers toapp(UID 1000) while the master runs asrootto avoid socket/permission surprises; workers remain unprivileged. - The nginx config contains conservative security headers and blocking of hidden files; review and extend headers (CSP, COEP, COOP) as needed per-site.
Production deployment and archival
- For archival (quiesce + tar), stop services and
tarthe./wp_rootand any associated volumes (database dump + attachments). Ensure services are fully topped to avoid inconsistent state.