#

Hosting

Subscribe
Hosting October 29, 2026·14 min read

Run WordPress with Docker Compose

Use Docker Compose for WordPress if you want control over a self-hosted installation and can maintain its data, updates, and recovery. A local setup starts with an Apache-based WordPress container, a MariaDB container, and two persistent volumes. Public hosting adds HTTPS, security, and tested-backup responsibilities.

Without a health check, WordPress can start before MariaDB is ready. Files outside a persistent volume are lost when the container is replaced.

The example binds to a loopback HTTP port, so you can verify the installation before exposing anything on a public domain.

Is Docker Compose the right way to run your WordPress site?

WordPress hosting choices

Compose suits you if you want the WordPress runtime, database, network, and storage defined in one project and you're ready to administer them. Before deploying a WordPress Compose stack, decide which images and versions you will pin and which named volume holds the database. Prove the data survives a container replacement.

SituationStarting pointWho handles operations?
Local development or testingWordPress and MariaDB in ComposeYou manage the local containers and test data.
Public site on your VPSCompose behind a configured HTTPS entry pointYou manage infrastructure security, updates, backups, and incidents.
Site owner seeking less server administrationManaged WordPress hostingThe provider handles the infrastructure responsibilities included in its service.
Developer who mainly needs a local editorLocalWP, DDEV, or another local development toolThe development tool manages much of the environment setup.

A developer looking to replace XAMPP started an r/Wordpress discussion with the question, “What's your go-to local development environment for WordPress in 2025?” Compose fits when you want direct control of the containers.

The example uses the Apache variant of the WordPress image. Apache handles HTTP requests inside the container, so you only need WordPress and MariaDB.

The PHP-FPM image works differently. It processes PHP through FastCGI and needs a web server such as Nginx in front of it to handle HTTP requests. Never expose its FastCGI port as the site's public HTTP endpoint.

The address http://localhost:8080 is a test endpoint on the Docker host.

Prepare the project, Docker prerequisites, and image versions

Docker project prerequisites

You need Docker Engine, the Docker Compose plugin, a free host port, enough disk space, and a writable project directory. The commands use the docker compose plugin syntax.

Check that Compose is available:

docker compose version

Create a project directory and enter it:

mkdir wordpress-compose
cd wordpress-compose

The project will contain these files:

wordpress-compose/
├── compose.yaml
├── .env
└── .gitignore

compose.yaml defines the services. .env supplies values that Compose substitutes into the configuration. .gitignore keeps the credentials out of Git.

Pin your image versions. The configuration below uses the WordPress Apache image with PHP 8.3 and MariaDB 11.8, both documented image options. Check plugin compatibility and database upgrades for your own site.

WordPress's hosting requirements list PHP 8.3 or newer, MariaDB 10.11 or newer or MySQL 8.0 or newer, and HTTPS support. For new WordPress 7.1 installations, the Hosting Team recommends PHP 8.4 or 8.5 with MySQL 8.4 or MariaDB 10.11, 11.4, or 11.8.

The wordpress:7.1.3-php8.3-apache tag names a WordPress image with PHP 8.3 and Apache. A tag can point to rebuilt image content. If you need the exact same image every time, record its digest.

The example publishes WordPress with 127.0.0.1:8080:80: host loopback address, host port 8080, and WordPress container port 80, in that order.

If Docker runs on a remote VPS, localhost means that VPS. To reach it from your own computer, use a secure route such as an SSH tunnel or a separately configured proxy.

Define WordPress, MariaDB, credentials, and service readiness in compose.yaml

Compose service configuration

WordPress and MariaDB need matching database credentials and a shared internal network. MariaDB creates the named database and application user during initial setup, and WordPress connects to it by the Compose service name.

Create compose.yaml with this configuration:

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

  wordpress:
    image: wordpress:7.1.3-php8.3-apache
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "127.0.0.1:8080:80"
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: ${WP_DB_PASSWORD:?Set WP_DB_PASSWORD in .env}
    volumes:
      - wp_data:/var/www/html

volumes:
  db_data:
  wp_data:

The configuration builds on the Docker Official WordPress Image and MariaDB's documented initialization and health-check mechanisms.

Database service relationship

Match the database name, hostname, and credentials

The MariaDB service is named db. Compose exposes that service name on the internal network, so WordPress uses WORDPRESS_DB_HOST=db:3306.

Inside the WordPress container, localhost is the WordPress container itself, not MariaDB.

Both services use the database name wordpress, the username wordpress, and the same application password. WordPress connects with that application account. MariaDB's root account has its own administrator credential.

MariaDB reads MARIADB_DATABASE, MARIADB_USER, and the password settings when it initializes a new database directory. Its separate automatic-upgrade setting is the documented exception. The WordPress image uses its WORDPRESS_DB_* settings to connect to the database MariaDB created.

Provide the passwords

Create .env with placeholders, then replace both with different, strong, generated passwords:

WP_DB_PASSWORD=REPLACE_WITH_UNIQUE_APP_PASSWORD
DB_ROOT_PASSWORD=REPLACE_WITH_DIFFERENT_ROOT_PASSWORD

Add the credential file to .gitignore:

.env

Restrict access to the file on the host and keep it out of version control. Compose substitutes the values into the explicit service environment settings. A .env file alone doesn't inject every value into every container.

Interpolated passwords are visible in a rendered Compose configuration and in the container environment, so don't share output from commands that print them.

WordPress credentials map

Compose also supports secrets granted to a service and mounted as files under /run/secrets/. The WordPress image supports _FILE password variables that read credentials from those files.

Wait for MariaDB readiness

Compose can start a database container before the database accepts application connections. The db health check runs MariaDB's documented healthcheck.sh with --connect and --innodb_initialized.

With depends_on set to condition: service_healthy, Compose waits for the database to pass that check before it starts WordPress.

The health check only covers the database. You verify WordPress itself in the browser.

Store WordPress files and database data where containers cannot accidentally discard them

Persistent Docker volumes

WordPress files and the MariaDB database need separate persistent storage. The configuration uses Docker named volumes, which keep the data out of the containers' disposable writable layers.

StorageContentsContainer location
db_dataMariaDB database files, including WordPress posts and settings stored in the database/var/lib/mysql
wp_dataWordPress application files, uploads, themes, plugins, and relevant filesystem configuration/var/www/html
Container writable layerChanges outside persistent mountsSpecific to the container
Optional bind mountA host directory shared into the containerThe destination selected in the mount definition

You can replace a container and keep its named volume. A bind mount exposes a host directory you choose. That's convenient for local theme development, but it brings host and container file-ownership issues.

A Linux developer mounting wp-content for child-theme work described the problem in an r/docker discussion: “If I use $USER:$USER, then the WordPress installation doesn't have sufficient permissions.” Your editor and the container's web-server process need different access to the same files. Setting mode 777 on the whole site doesn't solve ownership, and it's unsafe.

Docker storage lifecycle

Protect the named volumes. A plain docker compose down removes the project's containers and networks and keeps the declared named volumes. docker compose down -v also removes declared named volumes and attached anonymous volumes. External volumes are excluded. Never use the -v form as a routine reset on site data you want to keep.

Project names matter too. Rename the project and Compose can create or select a different set of volume names, leaving your original data in the earlier project's volumes.

Write down the two named volumes and their mount paths.

Validate the Compose file and start the WordPress stack

Compose startup commands

Validate the configuration before creating the services. From the directory containing compose.yaml, run:

docker compose config -q

This checks the configuration quietly and doesn't print the rendered service model. Fix any syntax or interpolation errors it reports before starting the stack.

Start WordPress and MariaDB in detached mode:

docker compose up -d

Inspect service state:

docker compose ps

Expect MariaDB to turn healthy first, then WordPress to run. Initial database preparation takes time, so the health check can stay pending before it passes.

If the database reports a failure or stays unhealthy, read its logs:

docker compose logs --tail=50 db

To read the WordPress logs:

docker compose logs --tail=50 wordpress

Complete WordPress setup and verify that your data survives recreation

WordPress persistence test

On the machine running Docker, open http://localhost:8080. Once WordPress connects to MariaDB, you should see its installation screen.

Choose the site's language, title, administrator username, and administrator password. Complete the installation and sign in to the dashboard.

Publish a harmless, easy-to-spot post. For example, one titled:

Docker persistence test

Upload a small disposable image through the Media Library too. The post tests database content. The image tests the WordPress filesystem.

Recreate the WordPress application container

From the same Compose project directory, run:

docker compose up -d --no-deps --force-recreate wordpress

This recreates the WordPress container and keeps its configured volume. --no-deps leaves MariaDB running.

WordPress content retention

When WordPress is back up, reload the site. Sign in and confirm the test post is still there.

This test covers one case: you replace the WordPress container while MariaDB keeps running. Recreating the database container or restoring from an off-host backup needs its own test.

The local hostname is part of the installation's state, so a later move from localhost to a public domain also means updating the WordPress URL and proxy configuration.

Troubleshoot database errors, unhealthy containers, and file permissions

WordPress error diagnosis

When WordPress fails, read the service state and logs to find which layer broke before changing any stored data.

SymptomPossible failure layerFirst focused check
Compose fails before starting servicesConfiguration, interpolation, image retrieval, or host setupRead the reported Compose error.
MariaDB stays unhealthyInitialization, data directory, or database readinessExamine the database health status and error logs.
WordPress shows a database connection errorHostname, database name, application account, password, or stored DB stateCompare WordPress's connection settings with the actual database.
The browser cannot reach the siteHost port, binding address, or application serviceCheck the address being opened and the configured port mapping.
WordPress cannot write or upload filesMount ownership or runtime permissionsInspect the affected path and process identity.

When WordPress cannot connect to MariaDB

A reachable database port proves network access only. WordPress still needs the right database name and an account allowed to connect. Compare WORDPRESS_DB_HOST, WORDPRESS_DB_NAME, WORDPRESS_DB_USER, and WORDPRESS_DB_PASSWORD against the database that exists.

In this project, the database hostname is db:3306.

Editing a password in .env after the first start doesn't change the password stored for the existing database account.

Container failure isolation

When WordPress cannot write to its files

With bind-mounted directories, the host file owner and the container process user can differ. Check the ownership and permissions of the files in question and the user the process runs as.

What changes before you put WordPress Docker Compose on a public domain?

Public WordPress networking

A public WordPress deployment needs an HTTPS entry point, domain routing, controlled service exposure, and ongoing administration. The local example has no public TLS endpoint.

Three addresses play different roles:

  • Container port 80: Apache serves HTTP inside the WordPress container.
  • Host port 8080: The local example publishes WordPress on the Docker host's loopback address.
  • Public HTTPS endpoint: A separate entry point receives requests for your domain over TLS.

Without a host address, Docker publishes a port on every host interface. The 127.0.0.1:8080:80 mapping in this example limits it to loopback.

MariaDB has no published host port in this configuration, and public visitors have no reason to reach it.

A reverse proxy such as Nginx, Caddy, or Traefik can receive public HTTPS traffic and forward requests to WordPress.

Why WordPress redirects can use the wrong scheme or port

WordPress builds links and redirects from its configured site URLs and request information. When a reverse proxy terminates TLS, the application has to receive reliable information that the original request used HTTPS.

The official WordPress Docker image already handles the HTTP_X_FORWARDED_PROTO header, so you don't always need a WORDPRESS_CONFIG_EXTRA snippet for it.

The proxy has to set the forwarded request information itself, in a trusted configuration. Don't treat client-controlled forwarding headers as proof that the request was secure.

HTTPS deployment checks

Before going public, check these:

  • DNS: Your public hostname resolves to the entry point.
  • TLS: The certificate validates for that hostname, and the browser reaches the site over HTTPS.
  • WordPress URLs: Redirects and generated links keep the intended scheme, hostname, and external port.
  • Network access: Only the services meant for outside access are reachable from the internet.

Back up both parts of WordPress and prove the backup can restore

WordPress restore validation

Recovering WordPress requires both the database and the site files. Posts and many site settings live in the database. Uploads, themes, plugins, and other application files sit in the WordPress filesystem.

A volume that survives container recreation protects that local state. It doesn't help when the volume itself is deleted, corrupted, or unavailable. For that you need an independently stored backup.

In another r/docker discussion, a self-hoster considering backup automation asked, “For those running Nextcloud or WordPress in Docker: how do you handle backups?” What matters technically is whether the database and filesystem captures together make a usable copy of the site.

A transactional dump with an option such as mysqldump --single-transaction gives a consistent view of supported transactional database operations. It doesn't coordinate with media uploads or other filesystem writes happening at the same time.

If the site keeps changing during the backup, use a coordinated capture process or a maintenance window. Keep the database snapshot, the required files, and the protected configuration needed to reconnect the restored site.

Define a restoration acceptance test

Restore a backup into an isolated environment with its own project name and storage, so the test can't overwrite the working site. The recovered instance should pass these checks:

  • WordPress authenticates to the recovered database.
  • A known published post and representative settings are present.
  • A known uploaded image, the required themes, and your plugins are available.
  • An administrator can sign in.
  • Links and media paths work under the test hostname.
Isolated site restoration

A backup command that exits cleanly only tells you the tool finished. Restore it into an isolated copy to see whether the database and files rebuild a working site.

Maintain and update the WordPress Compose installation without losing track of state

WordPress update planning

WordPress maintenance involves several components that change independently: the container image, the persisted WordPress core files, plugins and themes, and the MariaDB server. An image update can leave application files already in the volume untouched.

Compose can download the images declared in the project without starting the services:

docker compose pull

Pulling doesn't apply the image to a running container. That's a separate step. A WordPress update and a MariaDB data-format upgrade each have their own behavior and compatibility requirements.

Before each change, keep a short operating record:

  • Application image: the WordPress image reference, the PHP/Apache variant, and how you'll replace it.
  • Application state: installed WordPress core, plugin, and theme versions, plus any compatibility constraints.
  • Database: the MariaDB release and any requirements for upgrading its existing data directory.
  • Recovery: the backup and restore method you have before a major change.
  • Post-change checks: service readiness, administrator login, a few posts and media files, and error logs.

Should you use Docker Compose for WordPress? The deciding rule

WordPress deployment decision

If you can't maintain HTTPS, security, and tested backups on a public server, use managed hosting and keep Compose for local work.

NoScript Team