Skip to content

How to Run WordPress with Docker Compose

Run WordPress with Docker Compose: a working wordpress + MySQL stack with named volumes for persistence, env vars, ports, a reverse proxy note, and backups.

SDSysadmin Desk July 26, 2026 8 min read
Diagram-style cover showing a WordPress container and a MySQL container connected by Docker Compose with named volumes for data

WordPress needs two things to run: PHP serving the site and a MySQL database behind it. Installing both directly on a server works, but it scatters PHP modules, a web server config, and a database across the host, and moving or rebuilding it later is a chore. Docker Compose keeps the whole thing in one file you can read, version, and bring up with a single command.

This guide builds a complete WordPress site with Compose: the official wordpress image talking to mysql, with named volumes so your posts and uploads survive container rebuilds. You’ll get the working compose file, an explanation of every part, how to keep credentials out of the file, and short sections on putting it behind a reverse proxy and backing it up.

If you’re new to Compose files, the Docker Compose beginner guide covers the syntax this builds on — this article is the practical, production-leaning version of that example.

The two-container shape

A WordPress stack is two services that talk over an internal network:

  • db — MySQL, holding all the dynamic content: posts, pages, users, settings.
  • web — the WordPress image (PHP plus Apache), serving the site and connecting to db.

WordPress reaches the database by the service name db, not an IP address, because Compose runs an internal DNS resolver on the network it creates. That’s why you’ll see WORDPRESS_DB_HOST: db in the config — db is simply the name of the database service.

The complete compose file

Create an empty folder and save this as compose.yaml. It’s a full, working site.

services:
  db:
    image: mysql:8.0
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wpuser
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
    volumes:
      - db-data:/var/lib/mysql

  web:
    image: wordpress:6.5
    restart: unless-stopped
    depends_on:
      - db
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_USER: wpuser
      WORDPRESS_DB_PASSWORD: ${DB_PASSWORD}
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - wp-content:/var/www/html/wp-content

volumes:
  db-data:
  wp-content:

Read top to bottom, it’s a complete description of the site. db runs MySQL 8.0, stores its files in the db-data named volume, and gets its database name and passwords from environment variables. web runs WordPress 6.5, waits for db to be created, publishes container port 80 as 8080 on the host, points WORDPRESS_DB_HOST at the db service, and keeps its uploads, themes, and plugins in the wp-content volume. The volumes: block declares both named volumes so Docker manages them.

Bring it up from that folder:

docker compose up -d

Give MySQL a minute to initialize on the first run, then open http://localhost:8080 and you’ll land on the WordPress installer.

Why the volumes matter

This is the part that separates a site you can keep from one that vanishes on a rebuild. A container’s own filesystem is disposable — recreate the container and anything written inside it is gone. WordPress writes data in two places, and both need a named volume:

  • db-data/var/lib/mysql holds the database: every post, page, comment, user, and setting.
  • wp-content/var/www/html/wp-content holds uploaded media, installed themes, and plugins.

Map those to named volumes and the data lives independently of the containers. Update the WordPress image, recreate the container, and the volume reattaches with everything intact.

What lives where in the stack

db-data volume MySQL database files — posts, users, settings. Lose this and the site is empty.
wp-content volume Uploads, themes, plugins. Lose this and media and customizations are gone.
WordPress core files Inside the web image; recreated from the image, so no volume needed.
Environment variables DB connection and credentials — set in the file or a .env.

Keep credentials out of the file

The compose file above uses ${DB_PASSWORD} and ${DB_ROOT_PASSWORD} instead of literal passwords. Compose reads those from a .env file in the same folder automatically, which keeps secrets out of the file you commit to git.

Create .env next to your compose.yaml:

DB_PASSWORD=use-a-strong-password-here
DB_ROOT_PASSWORD=use-a-different-strong-password

Then make sure it never lands in version control:

.env

Don’t publish the database port

Notice that the db service has no ports: section. That’s deliberate. WordPress reaches MySQL over the internal Compose network by the service name db, so the database port never needs to be exposed to the host or the network.

Publishing MySQL with something like -p 3306:3306 would put the database on the host’s interfaces — and on a server with a public IP and no firewall, that can mean an exposed database. The only port that needs to be reachable from outside is WordPress’s web port. Leave MySQL unpublished and it’s only reachable by the WordPress container that needs it.

Putting it behind a domain and HTTPS

Port 8080 is fine for testing, but a real site needs a domain and HTTPS. The clean way to do that is a reverse proxy in front of WordPress, rather than wiring TLS into the WordPress container itself.

The proxy listens on 80 and 443, terminates HTTPS, and forwards requests to the WordPress container over the internal network. WordPress stays on plain HTTP internally and reads the forwarded headers to know the original request was HTTPS. That same proxy can host several sites on one server behind one set of ports. The full setup — shared network, proxy_pass, TLS, and the headers WordPress needs — is in running an Nginx reverse proxy with Docker.

One WordPress-specific note when you move behind a proxy and domain: set the site URL correctly (via the WORDPRESS_CONFIG_EXTRA environment variable or in WordPress settings) so it builds links with your real https:// domain instead of localhost:8080.

Backing up the site

A WordPress Docker site has two things worth backing up, and they need different methods.

The database should be dumped with mysqldump, not copied as raw files — a live database file copy can be inconsistent:

docker exec <db-container-name> \
  sh -c 'exec mysqldump -uroot -p"$MYSQL_ROOT_PASSWORD" wordpress' \
  > wordpress-db-$(date +%F).sql

The wp-content volume (uploads, themes, plugins) is static enough to archive with the tar-through-a-container method:

docker run --rm \
  -v wp-content:/data \
  -v "$PWD":/backup \
  alpine tar czf /backup/wp-content-$(date +%F).tar.gz -C /data .

Together those two files are a full restore point. There’s a complete walkthrough — including restoring and scheduling with cron — in how to back up Docker volumes.

WordPress on Compose — before you call it done

  • db-data and wp-content are named volumes (data survives a rebuild)
  • Passwords come from a .env file, and .env is in .gitignore
  • The db service has no published port — MySQL stays internal
  • Image tags are pinned (mysql:8.0, wordpress:6.5), not latest
  • A reverse proxy handles the domain and HTTPS for production
  • Both the database (mysqldump) and wp-content (tar) are backed up on a schedule

Wrapping up

A WordPress site in Compose is two services — wordpress and mysql — wired together by a service name, with named volumes doing the real work of keeping your data alive across rebuilds. Get four things right and the rest follows: put the database and wp-content on named volumes, keep credentials in a .env file, leave MySQL unpublished, and pin your image tags.

For production, add a reverse proxy for the domain and TLS, and put a backup schedule in place from day one. The pieces connect: the Compose beginner guide for the fundamentals, the reverse proxy guide for HTTPS and multiple sites, and backing up Docker volumes so a bad down -v is recoverable instead of fatal. More container walkthroughs live in the Docker & Containers guides.

Frequently asked questions

Why run WordPress in Docker instead of installing it directly?

Docker keeps WordPress, PHP, and MySQL in defined containers instead of installed on the host, so the setup is repeatable and easy to move or rebuild. One compose file describes the whole site, you can version it in git, and tearing it down doesn't leave PHP and MySQL scattered across the server. Updating is a matter of changing an image tag and re-running up.

Where does WordPress store its data in a Docker setup?

In two places. The database (posts, users, settings) lives in MySQL's data directory, and uploaded media plus themes and plugins live in WordPress's wp-content folder. Map both to named volumes so they survive container recreation. Without volumes, recreating a container wipes that data.

Do I need to expose the MySQL port in the compose file?

No. WordPress reaches MySQL over the internal Compose network using the service name as the hostname, so the database port never needs publishing to the host. Leaving MySQL unpublished keeps it off your network entirely, which is the safer default. Only the WordPress web port needs to be reachable from outside.

How do I put WordPress behind a domain and HTTPS?

Run a reverse proxy in front of the WordPress container and let it handle the domain and TLS. The proxy listens on 80 and 443, terminates HTTPS, and forwards to WordPress over the internal network, so WordPress itself stays on plain HTTP internally. This also lets you host several sites on one server behind one set of ports.

How do I back up a WordPress Docker site?

Back up two things: the MySQL database with mysqldump, and the wp-content volume with a tar archive through a temporary container. The database dump captures posts and settings; the wp-content backup captures uploads, themes, and plugins. Together they're a full restore point. Schedule both so they run automatically.

Why does WordPress show 'Error establishing a database connection'?

Usually the database isn't ready yet, or the credentials don't match. On first start MySQL needs a minute to initialize before it accepts connections, so WordPress may show the error briefly — wait and refresh. If it persists, check that WORDPRESS_DB_USER, WORDPRESS_DB_PASSWORD, and WORDPRESS_DB_NAME exactly match the MySQL environment variables, and that WORDPRESS_DB_HOST is the db service name.

Sources & further reading

Official vendor documentation referenced while writing this guide.

SD

Sysadmin Desk

Infrastructure & Cloud

Hands-on guidance for infrastructure, virtualization, and containers — Hyper-V, VMware, Docker, and the day-to-day operations work that keeps environments running.

MCSA Guru provides independent, educational IT guidance. Microsoft, Windows, Windows Server, Microsoft 365, Exchange, and Microsoft Teams are trademarks of Microsoft Corporation; Docker is a trademark of Docker, Inc. MCSA Guru is not affiliated with or endorsed by Microsoft or Docker. Always test changes in a safe environment before applying them in production.

Related guides

Diagram-style cover showing a temporary container archiving a Docker named volume to a tar file on the host

How to Back Up Docker Volumes

Back up Docker named volumes the reliable way: tar a volume via a temp container, restore it, dump databases properly, schedule it, and move data between hosts.

Sysadmin Desk Jul 19, 2026 8 min read

Fixing something right now?

Jump straight into the guide library or search for the exact error or task you are dealing with.