Docker Hub - Quay.io - bfren ecosystem
WordPress, with the core files kept read-only inside the image, and everything that changes - themes, plugins, uploads and wp-config.php - in one volume.
It is Nginx + PHP with:
- WordPress 7.1.3 (British English) installed read-only, so core updates come from pulling a new image and can't be changed by plugins or attackers
- the PHP extensions WordPress requires and recommends, including Imagick and Redis (PHP 8 builds)
wp-config.phpgenerated from environment variables, with secret keys downloaded once and kept- WordPress's scheduled tasks run every minute by the system's cron, instead of on page loads
- the WP-WebAuthn plugin (passkey logins) and, optionally, NinjaFirewall installed for you
- nginx rules that block access to hidden files and to PHP files in uploads
It is designed to run behind a reverse proxy that handles SSL, such as Caddy Proxy.
With a MariaDB database (e.g. bfren/mariadb):
services:
wordpress:
image: quay.io/bfren/wordpress:php8.4-7.2
environment:
- BF_WP_DB_HOST=db
- BF_WP_DB_NAME=wordpress
- BF_WP_DB_USER=wordpress
- BF_WP_DB_PASS=change-me
ports:
- "8080:80"
volumes:
- ./v/wp-content:/wp-contentOpen the site and follow WordPress's installation (site title and admin user).
- π Read-only WordPress core, with minor updates only through new images
- π
wp-config.phpgenerated from environment variables at every start - β° System cron for WordPress's scheduled tasks
- π‘οΈ Optional NinjaFirewall in full web application firewall mode, and WebAuthn passkey logins
- β‘ Redis object cache settings, for use with a Redis plugin
- π PHP 7.4 to 8.5 - choose with the image tag
- ποΈ Everything from Nginx + PHP: PHP-FPM, OPcache, compression and logs
- π©Ί Health check that loads the login page
Images are published to Docker Hub (bfren/wordpress) and Quay.io (quay.io/bfren/wordpress) for linux/amd64 and linux/arm64. Tags start with the PHP version, and can include the WordPress version.
| Tag | Example | Use when |
|---|---|---|
phpX.Y |
php8.4 |
You want the latest image for a PHP version |
phpX.Y-MAJOR |
php8.4-7 |
You want updates, but not breaking changes |
phpX.Y-MINOR |
php8.4-7.2 |
You want fixes only - recommended |
phpX.Y-VERSION |
php8.4-7.2.0 |
You want to pin this image exactly |
phpX.Y-wpA.B |
php8.4-wp7.1 |
You want to stay on a WordPress version |
phpX.Y-wpA.B.C |
php8.4-wp7.1.3 |
You want an exact WordPress release |
β¦-VERSION versions of both |
php8.4-wp7.1.3-7.2.0 |
You want to pin both |
phpX.Y.Z |
php8.4.26 |
You want an exact PHP release |
There is no latest tag. PHP 8.1 and older no longer receive security fixes - see Nginx + PHP for support dates.
| Port | Purpose |
|---|---|
80 |
HTTP - put a reverse proxy in front of it for SSL. |
| Volume | Purpose |
|---|---|
/wp-content |
Themes, plugins, uploads and languages, plus wp-config.php and salt (the secret keys). Back it up. |
| Variable | Values | Description | Default |
|---|---|---|---|
BF_WP_DB_HOST |
host name | Required. Database server. | blank |
BF_WP_DB_NAME |
text | Required. Database name. | blank |
BF_WP_DB_USER |
text | Required. Database user. | blank |
BF_WP_DB_PASS |
text | Required. Database password. | blank |
BF_WP_DB_PREFIX |
text | Table prefix - an underscore is added (e.g. wp_). |
wp |
BF_WP_DB_CHARSET |
text | Database character set. | utf8mb4 |
BF_WP_DB_COLLATE |
text | Database collation - leave blank unless you know you need it. | blank |
BF_WP_DEBUG |
true, false |
WordPress debug mode. | false |
BF_WP_CONFIG_REGENERATE |
0, 1 |
1 generates wp-config.php at every start. 0 only generates it if it doesn't exist, so you can edit it yourself. |
1 |
BF_WP_CONFIG_HARDEN |
0, 1 |
1 makes wp-config.php read-only to WordPress. 0 makes it writable, for plugins that need to change it. |
1 |
BF_WP_CONFIG_ADDITIONAL |
PHP code | Extra lines added to wp-config.php, e.g. define( 'WP_MEMORY_LIMIT', '256M' );. |
blank |
BF_WP_ENABLE_HTTPS_BEHIND_PROXY |
0, 1 |
Treat requests as HTTPS when the proxy sends X-Forwarded-Proto: https. |
1 |
BF_WP_USE_SYSTEM_CRON |
0, 1 |
Run scheduled tasks from the system's cron every minute, instead of during page loads. | 1 |
BF_WP_DISABLE_MAJOR_UPDATES |
0, 1 |
Limit WordPress's automatic core updates to minor releases. | 1 |
BF_WP_INSTALL_WEBAUTHN |
0, 1 |
Install the WP-WebAuthn plugin (you still activate it in WordPress). | 1 |
BF_WP_INSTALL_NINJAFIREWALL |
0, 1 |
Install NinjaFirewall and set up full firewall mode - only after WordPress is installed (see NinjaFirewall). | 0 |
BF_WP_REDIS_HOST |
host name | Redis server for an object cache plugin. | blank |
BF_WP_REDIS_PORT |
port | Redis port, if not 6379. | blank |
BF_NGINX_HEALTHCHECK_URI is set to http://localhost/wp-login.php. Nginx + PHP adds the PHP settings, and Nginx the nginx ones.
When the container starts, Nginx + PHP sets up nginx and PHP, then this image:
- Content - if
/wp-contentis empty, fills it with WordPress's default content, then links WordPress's ownwp-contentfolder to it - Configuration - generates
/wp-content/wp-config.phpfrom the environment variables (or keeps yours, withBF_WP_CONFIG_REGENERATE=0) and links it into WordPress. The secret keys are downloaded from WordPress.org the first time and saved in/wp-content/salt, so logins survive restarts - Plugins - installs WP-WebAuthn and NinjaFirewall, if enabled and not already installed
- Permissions - makes
/wp-contentowned by thewwwuser, andwp-config.phpread-only (or writable, withBF_WP_CONFIG_HARDEN=0)
If any step fails - e.g. a required database variable is missing - the container stops. Check docker logs.
The WordPress core files are read-only, so WordPress can't update itself - pull a new image instead. Plugins and themes are in /wp-content, so update them from WordPress as normal. The "background updates" check in Site Health is removed, as it would always fail.
With BF_WP_USE_SYSTEM_CRON=1, WordPress's own cron is turned off in wp-config.php, and the system cron runs wp-cron every minute, which requests http://localhost/wp-cron.php. Run it yourself with docker exec <container> wp-cron.
With BF_WP_INSTALL_NINJAFIREWALL=1, the plugin is installed and two files are generated so it runs in full web application firewall mode (before WordPress loads). Turn this on only after WordPress has been installed - otherwise the firewall blocks the installation. Then activate and set up the plugin in WordPress.
The site blocks hidden files (e.g. .htaccess) and PHP files in uploads (helpers/wp-restrictions.conf), and routes "pretty" permalinks to index.php. helpers/wp-subdirectory.conf is an example for running WordPress in a /wordpress sub-folder - copy it and change the name.
The WordPress version and language come from overlay/tmp/WP_VERSION and overlay/tmp/WP_LOCALE. To build a different one without changing the files, pass build arguments:
docker buildx build --build-arg WP_LOCALE=de_DE --build-arg WP_VERSION=7.1.3 -f php8.4/Dockerfile -t my-wordpress .The version must be a released WordPress version, and the locale one that WordPress.org publishes downloads for, or the build fails. Published images always use the files.
./test.nu # builds the image and runs /test
./test.nu --all # every version, in parallel/test runs this image's tests alongside the base images' tests, and fails if tests don't execute. They generate wp-config.php from its template and check it is valid PHP - including with quotes and backslashes in the database password - and that each optional setting appears only when enabled. They also check the build arguments, wp-config.php permissions, that existing secret keys are kept, recreating the default content, the installed WordPress version, plugin versions and PHP extensions, and that nginx accepts the configuration.
Set all four database variables: BF_WP_DB_HOST, BF_WP_DB_NAME, BF_WP_DB_USER and BF_WP_DB_PASS.
It is regenerated at every start. Use BF_WP_CONFIG_ADDITIONAL for extra settings, or set BF_WP_CONFIG_REGENERATE=0 to manage the file yourself.
Set BF_WP_CONFIG_HARDEN=0.
Check the proxy sends X-Forwarded-Proto: https, and that BF_WP_ENABLE_HTTPS_BEHIND_PROXY is 1.
Core updates come with the image - pull the latest tag and recreate the container.
So nothing - a plugin, or an attacker - can change WordPress's own files. Themes, plugins and uploads are in /wp-content as normal; only the core files come from the image.
Yes - set BF_WP_REDIS_HOST (and BF_WP_REDIS_PORT if it isn't 6379) and install a Redis object cache plugin. The Redis extension is included in PHP 8 builds.
Copyright (c) 2020-2026 bfren (unless otherwise stated)