Independent tech intelligence, checked against primary sources.

TechPulseMind Useful technology.
No manufactured hype.

How to Set Up WordPress with Docker Compose

Deploy WordPress with Docker Compose: persistent storage, healthchecks, and optional log rotation – a step‑by‑step tutorial.

How to Set Up WordPress with Docker Compose

Before you start: versions, package names and storage identifiers change over time. Check the commands against the official documentation for your setup before running them, especially anything that creates, deletes or overwrites data.

A WordPress stack in Docker is two services — WordPress and a database — plus the persistence and secrets handling that decide whether it survives a restart or an update. This guide sets up a stack you can actually keep.

Prerequisites

  • A Linux host with Docker Engine and the Compose plugin (docker compose version should answer)
  • A free port for the site — this guide uses 8080

Step 1: Project directory and secrets

mkdir -p ~/wordpress && cd ~/wordpress

Generate passwords into a .env file rather than typing them into the Compose file:

cat > .env <<EOF
DB_ROOT_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=')
DB_PASSWORD=$(openssl rand -base64 24 | tr -d '/+=')
EOF
chmod 600 .env

The tr strips characters that cause quoting problems in shell and YAML contexts. chmod 600 matters because this file is now the only thing standing between a local user and your database.

Add .env to .gitignore immediately if this directory is a repository. Compose reads .env from the project directory automatically.

Step 2: The Compose file

Create compose.yaml:

services:
  db:
    image: mariadb:11
    restart: unless-stopped
    environment:
      MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
      MARIADB_DATABASE: wordpress
      MARIADB_USER: wordpress
      MARIADB_PASSWORD: ${DB_PASSWORD}
    volumes:
      - db_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 10

  wordpress:
    image: wordpress:php8.3-apache
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: ${DB_PASSWORD}
    volumes:
      - wp_data:/var/www/html

volumes:
  db_data:
  wp_data:

Several choices there are deliberate.

No version: key. The top-level version property is obsolete — Docker’s docs state it “is only informative and you’ll receive a warning message that it is obsolete if used”, and that Compose “always uses the most recent schema to validate the Compose file, regardless of the version field”. Files starting with version: "3.9" are copying a format that no longer does anything.

The database publishes no port. There is no ports: entry on db, and that is intentional. WordPress reaches it over the Compose network by the service name db. Adding 3306:3306 exposes your database to the whole network for no benefit — it is one of the most common mistakes in copied WordPress compose files.

depends_on with condition: service_healthy. A plain depends_on only waits for the container to start, not for MariaDB to accept connections. Without the health condition, WordPress frequently starts first and fails its database connection on a cold boot. The healthcheck.sh script ships inside the official MariaDB image.

Pinned image tags. mariadb:11 and wordpress:php8.3-apache rather than :latest. A latest database tag means an unattended docker compose pull can move you across a major version, which is how people discover their backups were untested.

Step 3: Start it

docker compose config
docker compose up -d
docker compose ps

docker compose config renders the file with .env substituted in. Run it once: if a variable is unset, it shows up here as an empty value rather than as a confusing database error later.

Wait for db to report healthy in docker compose ps, then open http://<host-ip>:8080 and complete the WordPress installer.

Step 4: Confirm persistence

This is the step that distinguishes a working stack from one that looks like it works:

docker compose down
docker compose up -d

Your site should come back with its content. docker compose down removes containers but keeps named volumes.

The command to be careful with is:

docker compose down -v

-v deletes the named volumes, which means your database and uploads. There is no confirmation prompt.

Putting it behind a reverse proxy

Serving on port 8080 is fine for a LAN. For a real domain with TLS, terminate at a reverse proxy and tell WordPress it is behind one, or it generates http:// URLs and mixed-content breakage.

Add to the wordpress service:

    environment:
      WORDPRESS_CONFIG_EXTRA: |
        define('WP_HOME', 'https://example.com');
        define('WP_SITEURL', 'https://example.com');
        if (isset($$_SERVER['HTTP_X_FORWARDED_PROTO']) && $$_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
            $$_SERVER['HTTPS'] = 'on';
        }

Note the doubled $$. Compose performs its own variable substitution, so a single $ would be consumed by Compose instead of reaching PHP.

Your proxy must send X-Forwarded-Proto for that check to work. If you would rather not open ports at all, a Cloudflare Tunnel does the same job from the outbound side.

Updating

docker compose pull
docker compose up -d

Back up before this, not after. WordPress runs database migrations on version upgrades, and those are not reversible by pulling the old image back — the schema has already changed. The database dump is the rollback.

Common failures

Symptom Cause
“Error establishing a database connection” on first boot depends_on without condition: service_healthy
Site empty after down/up -v was used, deleting the volumes
Empty password in docker compose config .env missing, misnamed, or not in the project directory
Redirect loop or mixed content behind a proxy HTTP_X_FORWARDED_PROTO not handled — see above
Cannot upload large media PHP limits; raise upload_max_filesize via a custom php.ini

Sources

Related reading

Some links on this page may be affiliate links. If you buy through them we may earn a commission at no extra cost to you. See our affiliate disclosure.