Docker Compose — WordPress + MySQL Project

2 minute read

Build a complete WordPress blog with MySQL database and phpMyAdmin administration panel — a classic introduction to multi-container Compose setups.

Architecture

┌─────────────────────────────────────────────────┐
│                Docker Network: wp-net            │
│                                                  │
│  ┌──────────┐  ┌──────────┐  ┌───────────────┐  │
│  │WordPress │  │  MySQL   │  │  phpMyAdmin    │  │
│  │  :8080   │→ │  :3306   │←─│    :8888       │  │
│  └──────────┘  └──────────┘  └───────────────┘  │
│       │             │                            │
│  wp-content/    db-data                          │
│  (bind mount)  (named vol)                       │
└─────────────────────────────────────────────────┘

Project Setup

Create a project directory:

mkdir wordpress-compose && cd wordpress-compose

Step 1 — The Compose File

Create compose.yml:

services:
  db:
    image: mysql:8.0
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wpuser
      MYSQL_PASSWORD: wppassword
    volumes:
      - db-data:/var/lib/mysql
    networks:
      - wp-net
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  wordpress:
    image: wordpress:latest
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wpuser
      WORDPRESS_DB_PASSWORD: wppassword
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - ./wp-content:/var/www/html/wp-content
    networks:
      - wp-net

  phpmyadmin:
    image: phpmyadmin:latest
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "8888:80"
    environment:
      PMA_HOST: db
      PMA_PORT: 3306
    networks:
      - wp-net

volumes:
  db-data:

networks:
  wp-net:
    driver: bridge

Step 2 — Launch the Stack

docker compose up -d

Expected output:

[+] Running 4/4
 ✔ Network wordpress-compose_wp-net      Created
 ✔ Volume "wordpress-compose_db-data"    Created
 ✔ Container wordpress-compose-db-1      Healthy
 ✔ Container wordpress-compose-wordpress-1  Started
 ✔ Container wordpress-compose-phpmyadmin-1 Started

Step 3 — Verify Services

Check all containers are running:

docker compose ps

Expected output:

NAME                                STATUS          PORTS
wordpress-compose-db-1              Up (healthy)    3306/tcp, 33060/tcp
wordpress-compose-phpmyadmin-1      Up              0.0.0.0:8888->80/tcp
wordpress-compose-wordpress-1       Up              0.0.0.0:8080->80/tcp

Visit the WordPress setup wizard:

open http://localhost:8080    # macOS
# or
xdg-open http://localhost:8080  # Linux

Visit phpMyAdmin at http://localhost:8888 (login: wpuser / wppassword).

Step 4 — Inspect the Database Volume

docker volume inspect wordpress-compose_db-data

Expected output:

[
    {
        "CreatedAt": "2024-01-15T10:30:00Z",
        "Driver": "local",
        "Labels": {
            "com.docker.compose.project": "wordpress-compose",
            "com.docker.compose.volume": "db-data"
        },
        "Mountpoint": "/var/lib/docker/volumes/wordpress-compose_db-data/_data",
        "Name": "wordpress-compose_db-data",
        "Options": null,
        "Scope": "local"
    }
]

Step 5 — Test Data Persistence

Stop and restart the stack:

docker compose down
docker compose up -d

Visit WordPress again — your site should still be configured. The MySQL data survived because it is stored in the named volume db-data.

Caution: docker compose down -v would delete the volume and destroy your database. Only use -v when you intentionally want a clean slate.

Step 6 — Add Environment File

Create .env:

MYSQL_ROOT_PASSWORD=rootpassword
MYSQL_DATABASE=wordpress
MYSQL_USER=wpuser
MYSQL_PASSWORD=wppassword
WORDPRESS_PORT=8080
PHPMYADMIN_PORT=8888

Update compose.yml to use variable substitution:

services:
  db:
    image: mysql:8.0
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    volumes:
      - db-data:/var/lib/mysql
    networks:
      - wp-net
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  wordpress:
    image: wordpress:latest
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "${WORDPRESS_PORT}:80"
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: ${MYSQL_USER}
      WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD}
      WORDPRESS_DB_NAME: ${MYSQL_DATABASE}
    volumes:
      - ./wp-content:/var/www/html/wp-content
    networks:
      - wp-net

  phpmyadmin:
    image: phpmyadmin:latest
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "${PHPMYADMIN_PORT}:80"
    environment:
      PMA_HOST: db
      PMA_PORT: 3306
    networks:
      - wp-net

volumes:
  db-data:

networks:
  wp-net:
    driver: bridge

Verify the configuration:

docker compose config

Enhancements to Try

  1. Add SSL with an Nginx reverse proxy in front of WordPress
  2. Add WP-CLI as a service for command-line WordPress management
  3. Add automated backups with a cron-based service that dumps the database
  4. Use Compose profiles — put phpMyAdmin behind a debug profile so it only starts in development

Quick Reference

Item Detail
WordPress image wordpress:latest
MySQL image mysql:8.0
WordPress port 8080:80
phpMyAdmin port 8888:80
DB health check mysqladmin ping -h localhost
Data volume db-data:/var/lib/mysql