Skip to content

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.

On your own machine, with no proxy, domain or certificate. From apps/wordpress/:

Terminal window
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 -d

Then open http://localhost:8080. Stop it again with the same command and down.

From apps/wordpress/:

  1. Copy the environment file and set APP_TRAEFIK_HOST and TZ:

    Terminal window
    cp .env.example .env
  2. Generate the database secrets:

    Terminal window
    mkdir -p .secrets
    openssl rand -base64 32 | tr -d '\n' > .secrets/db_pwd.txt
    openssl rand -base64 32 | tr -d '\n' > .secrets/db_root_pwd.txt
  3. Start it:

    Terminal window
    docker compose up -d
  4. 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).

  5. Apply the Apache hardening — after the installer has finished, because it writes .htaccess itself and would overwrite the additions:

    Terminal window
    docker compose exec wordpress-app bash -c \
    'cat /config/.htaccess-security >> /var/www/html/.htaccess'
  6. Open the route for the site’s audience: APP_TRAEFIK_ACCESS=access-public in .env and docker compose up -d for a public site, or a second router that keeps /wp-admin on access-vpn — the scenarios are in apps/wordpress/README.md. A fully internal site stays as shipped.

Terminal window
docker compose ps # two services, healthy

Then 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.

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.

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.

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.

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.

  1. Read the release notes. The step from 6.x to 7.x is a major change — check themes and plugins against it first.

  2. Back up the database and volumes/wordpress together.

  3. Raise APP_TAG in .env, then:

    Terminal window
    docker compose pull && docker compose up -d
  4. 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.

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.