WordPress
WordPress with a MariaDB database, PHP settings tightened, and a set of Apache rules applied after installation. Two containers.
Last run on a host 8 Nov 2026
Pinned at 7.1.4-php8.3-apache (released 8 Nov 2026) · a newer upstream release is not determinable
In the upstream images: 1 CRITICAL · 28 HIGH with a published fix — per image
To try it: Docker, nothing else. See Try it locally.
To deploy it behind the proxy: Traefik running and a domain, and an SMTP relay via plugin.
Try it locally
Section titled “Try it locally”On your own machine, with no proxy, domain or certificate. From apps/wordpress/:
cp .env.local.example .env.local
# The secrets ship empty; generate the ones this stack needs{ echo "DB_PASSWORD=$(openssl rand -base64 32 | tr -d '\n')" echo "DB_ROOT_PASSWORD=$(openssl rand -base64 32 | tr -d '\n')"} >> .env.local
docker compose -f docker-compose.local.yml --env-file .env.local up -dThen open http://localhost:8080. Stop it again with the same command and down.
Installation
Section titled “Installation”From apps/wordpress/:
-
Copy the environment file and set
APP_TRAEFIK_HOSTandTZ:Terminal window cp .env.example .env -
Generate the database secrets:
Terminal window mkdir -p .secretsopenssl rand -base64 32 | tr -d '\n' > .secrets/db_pwd.txtopenssl rand -base64 32 | tr -d '\n' > .secrets/db_root_pwd.txt -
Start it:
Terminal window docker compose up -d -
Over the VPN, open
https://<APP_TRAEFIK_HOST>and run the WordPress installer. It makes the first visitor the administrator, which is why the route ships closed (access-vpn). -
Apply the Apache hardening — after the installer has finished, because it writes
.htaccessitself and would overwrite the additions:Terminal window docker compose exec wordpress-app bash -c \'cat /config/.htaccess-security >> /var/www/html/.htaccess' -
Open the route for the site’s audience:
APP_TRAEFIK_ACCESS=access-publicin.envanddocker compose up -dfor a public site, or a second router that keeps/wp-adminonaccess-vpn— the scenarios are inapps/wordpress/README.md. A fully internal site stays as shipped.
Verify
Section titled “Verify”docker compose ps # two services, healthyThen load the site, log in to /wp-admin, and publish one post with an image.
The upload path and the database are separate failures, and only the second is
visible from the front page.
Going further
Section titled “Going further”Plugins are attack surface
Section titled “Plugins are attack surface”The stack takes a minimal approach: install what the site needs and remove the rest. Four categories are worth having from the start — an SMTP plugin, so WordPress mail goes through a relay rather than the local PHP mailer; time-based two-factor authentication on logins; a consent banner where the site is subject to GDPR; and a backup plugin writing to somewhere other than this host.
The specific plugin choices and the three approaches to restricting /wp-admin
are in
apps/wordpress/README.md.
Why tls-compatible
Section titled “Why tls-compatible”The stack names tls-compatible on the router’s tls.options label
(APP_TRAEFIK_TLS_OPTION=tls-compatible in .env). The profile exists for
older clients that must still connect, and a public site is visited by whatever
the visitor happens to run. Verify on a host whether tls-standard, the
deployment default, serves the visitors a blog still wants, then remove the
variable and the label.
What it sends outward
Section titled “What it sends outward”The core update check contacts api.wordpress.org and carries more than a
version: the PHP and MySQL versions, the locale, the number of sites and the
number of users, and whether multisite is enabled. Plugin and theme checks
follow the same path.
The official image exposes no environment variable for it. Switching it off
means a must-use plugin or an egress rule.
Backup
Section titled “Backup”| Database | MariaDB, container wordpress-db, database wordpress, user wp_user |
| Password | .secrets/db_pwd.txt |
| State | volumes/mysql (database) · volumes/wordpress (uploads, themes, plugins) |
| Rebuildable | WordPress core inside volumes/wordpress, restored from the image on upgrade |
| Quiescing | Not needed — the dump is consistent on its own. |
For borgmatic:
mariadb_databases: - name: wordpress container: wordpress-db username: wp_user password: "{credential file /srv/docker/apps/wordpress/.secrets/db_pwd.txt}"Posts are in the database; media and the active theme are in
volumes/wordpress/wp-content. A database-only restore gives a site where every
image is missing and the theme has reverted to the default.
The salts in wp-config.php are generated at first start and invalidate every
session when they change, so a restore that regenerates them signs all users out.
Restore order: database, then the application.
Updates
Section titled “Updates”-
Read the release notes. The step from 6.x to 7.x is a major change — check themes and plugins against it first.
-
Back up the database and
volumes/wordpresstogether. -
Raise
APP_TAGin.env, then:Terminal window docker compose pull && docker compose up -d -
Load the front page and the administration area. A theme or plugin that did not survive the version shows up on one of the two, not in the log.
Troubleshooting
Section titled “Troubleshooting”The hardening rules are gone after running the installer. WordPress rewrote
.htaccess. Re-run installation step 5 — the order matters.
WordPress sends no mail. The container has no local mail transport by design. Configure an SMTP plugin against your relay.
Uploads fail after a restore. Ownership on volumes/wordpress/wp-content no
longer matches the user Apache runs as inside the container.
Everyone is logged out after a restore. The salts in wp-config.php were
regenerated. Expected; users log in again.
Repository files
Section titled “Repository files”apps/wordpress/README.md— setup, plugin approach, wp-admin protectionapps/wordpress/UPSTREAM.md— source and upgrade checklist
Official sources
Section titled “Official sources”© 2026 SecDockBlueCode Apache-2.0Site content CC BY-NC 4.0