Docker Compose: WordPress + MySQL Complete Reference
Docker Compose reference: WordPress + MySQL
TL;DR
- Bottom line: A production-ready WordPress stack requires just three services (WordPress, MySQL/MariaDB, optional phpMyAdmin) defined in a single
docker-compose.ymlwith named volumes for data persistence and environment-based configuration. - Key tool/command:
docker compose up -dto start the stack;docker compose exec wordpress wpfor WP-CLI. - Watch out for: Forgetting named volumes for
/var/lib/mysqland/var/www/html-- without them,docker compose downdestroys all your data. - Works with: Docker Engine 20.10+, Docker Compose V2, WordPress 6.x, MySQL 8.0/8.4 or MariaDB 10.11/11.x, PHP 8.1-8.3.
Constraints
- NEVER use MYSQL_ALLOW_EMPTY_PASSWORD=yes in production -- always set a strong root password or use Docker secrets
- NEVER hardcode database passwords in docker-compose.yml for production -- use .env files or Docker secrets (_FILE suffix)
- ALWAYS use named volumes for MySQL data and wp-content persistence -- bind mounts risk permission issues and data loss
- NEVER expose MySQL port 3306 to 0.0.0.0 in production -- keep it on the internal Docker network only
- ALWAYS use the Docker service name (e.g., 'db') as WORDPRESS_DB_HOST -- never use 'localhost' or '127.0.0.1' from within containers
- WORDPRESS_DB_NAME must already exist on the MySQL server -- the WordPress container will NOT create it (MySQL's MYSQL_DATABASE creates it)
Quick Reference
Service Configuration
| Service | Image | Ports | Volumes | Key Env |
|---|---|---|---|---|
| WordPress | wordpress:6.7-php8.3-apache | 8080:80 | wordpress_data:/var/www/html | WORDPRESS_DB_HOST=db |
| MySQL 8.0 | mysql:8.0 | Internal only | db_data:/var/lib/mysql | MYSQL_ROOT_PASSWORD, MYSQL_DATABASE |
| MySQL 8.4 LTS | mysql:8.4 | Internal only | db_data:/var/lib/mysql | MYSQL_ROOT_PASSWORD, MYSQL_DATABASE |
| MariaDB 10.11 | mariadb:10.11 | Internal only | db_data:/var/lib/mysql | MARIADB_ROOT_PASSWORD, MARIADB_DATABASE |
| MariaDB 11 | mariadb:11 | Internal only | db_data:/var/lib/mysql | MARIADB_ROOT_PASSWORD, MARIADB_DATABASE |
| phpMyAdmin | phpmyadmin:5 | 8081:80 | None | PMA_HOST=db |
| WP-CLI | wordpress:cli-php8.3 | None | wordpress_data:/var/www/html | Same DB env as WordPress |
WordPress Environment Variables
| Variable | Required | Default | Purpose |
|---|---|---|---|
WORDPRESS_DB_HOST | Yes | -- | Database hostname (use service name) |
WORDPRESS_DB_USER | Yes | -- | Database username |
WORDPRESS_DB_PASSWORD | Yes | -- | Database password |
WORDPRESS_DB_NAME | Yes | -- | Database name (must exist) |
WORDPRESS_TABLE_PREFIX | No | wp_ | Table prefix (change for security) |
WORDPRESS_DEBUG | No | "" | Set to 1 to enable WP_DEBUG |
WORDPRESS_CONFIG_EXTRA | No | "" | Additional wp-config.php PHP code |
WORDPRESS_*_FILE | No | -- | Load any var from a file (Docker secrets) |
MySQL Environment Variables
| Variable | Required | Default | Purpose |
|---|---|---|---|
MYSQL_ROOT_PASSWORD | Yes* | -- | Root user password |
MYSQL_DATABASE | No | -- | Database to create on first run |
MYSQL_USER | No | -- | Non-root user to create |
MYSQL_PASSWORD | No | -- | Password for MYSQL_USER |
MYSQL_RANDOM_ROOT_PASSWORD | No* | -- | Generate random root password |
MYSQL_*_FILE | No | -- | Load any var from a file (Docker secrets) |
Decision Tree
START: What kind of WordPress Docker setup do you need?
├── Development environment (local machine)?
│ ├── YES → Use basic docker-compose.yml with phpMyAdmin + port 8080
│ │ ├── Need WP-CLI? → Add wordpress:cli service sharing same volumes
│ │ └── Need custom plugins? → Use custom Dockerfile extending wordpress image
│ └── NO ↓
├── Production deployment?
│ ├── YES → Use production compose with:
│ │ ├── .env file for secrets (never commit to git)
│ │ ├── Named volumes for data persistence
│ │ ├── restart: unless-stopped on all services
│ │ ├── No phpMyAdmin exposed publicly
│ │ ├── Reverse proxy (Nginx/Traefik) with SSL
│ │ └── Health checks on all services
│ └── NO ↓
├── Need MariaDB instead of MySQL?
│ ├── YES → Replace mysql image with mariadb, use MARIADB_* env vars
│ └── NO ↓
├── Need multisite WordPress?
│ ├── YES → Add WORDPRESS_CONFIG_EXTRA with WP_ALLOW_MULTISITE
│ └── NO ↓
└── DEFAULT → Start with the basic development compose file
Step-by-Step Guide
1. Create project directory and environment file
Set up the project structure with a .env file to keep secrets out of docker-compose.yml. [src4]
mkdir wordpress-docker && cd wordpress-docker
cat > .env << 'EOF'
MYSQL_ROOT_PASSWORD=change_me_root_2026
MYSQL_DATABASE=wordpress
MYSQL_USER=wordpress
MYSQL_PASSWORD=change_me_wp_2026
WORDPRESS_TABLE_PREFIX=wp_
EOF
echo ".env" >> .gitignore
Verify: cat .env shows your environment variables.
2. Create docker-compose.yml
Define WordPress, MySQL, and phpMyAdmin services with named volumes and health checks. [src1] [src2]
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
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
wordpress:
image: wordpress:6.7-php8.3-apache
restart: unless-stopped
depends_on:
db:
condition: service_healthy
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: ${MYSQL_USER}
WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD}
WORDPRESS_DB_NAME: ${MYSQL_DATABASE}
volumes:
- wordpress_data:/var/www/html
phpmyadmin:
image: phpmyadmin:5
restart: unless-stopped
depends_on:
db:
condition: service_healthy
ports:
- "8081:80"
environment:
PMA_HOST: db
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
volumes:
db_data:
wordpress_data:
Verify: docker compose config resolves without errors.
3. Start the stack
Launch all services and verify health. [src3]
docker compose up -d
docker compose ps
docker compose logs -f wordpress
Verify: curl -s -o /dev/null -w '%{http_code}' http://localhost:8080 returns 302 or 200.
4. Complete WordPress installation via WP-CLI
Automate the installation wizard with WP-CLI. [src1]
docker compose run --rm \
-e WORDPRESS_DB_HOST=db \
-e WORDPRESS_DB_USER=${MYSQL_USER} \
-e WORDPRESS_DB_PASSWORD=${MYSQL_PASSWORD} \
-e WORDPRESS_DB_NAME=${MYSQL_DATABASE} \
wordpress:cli-php8.3 \
wp core install \
--url="http://localhost:8080" \
--title="My WordPress Site" \
--admin_user=admin \
--admin_password=change_me_admin \
[email protected]
Verify: Visit http://localhost:8080 and see your site title.
5. Back up your data
Export database and wp-content files. [src5]
# Database backup
docker compose exec db \
mysqldump -u root -p"${MYSQL_ROOT_PASSWORD}" "${MYSQL_DATABASE}" \
> backup-$(date +%Y%m%d).sql
# WordPress files backup
docker compose exec wordpress \
tar czf /tmp/wp-content-backup.tar.gz -C /var/www/html/wp-content .
docker compose cp wordpress:/tmp/wp-content-backup.tar.gz ./
Verify: head -5 backup-*.sql shows MySQL dump header.
6. Restore from backup
Restore database and files from backup. [src5]
# Restore database
docker compose exec -T db \
mysql -u root -p"${MYSQL_ROOT_PASSWORD}" "${MYSQL_DATABASE}" \
< backup-20260227.sql
# Restore wp-content
docker compose cp ./wp-content-backup.tar.gz wordpress:/tmp/
docker compose exec wordpress \
tar xzf /tmp/wp-content-backup.tar.gz -C /var/www/html/wp-content
Verify: Check row count in posts table to confirm data restored.
Code Examples
Docker Compose: Production with Docker secrets
# docker-compose.prod.yml -- Production WordPress
services:
db:
image: mysql:8.4
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD_FILE: /run/secrets/db_password
volumes:
- db_data:/var/lib/mysql
secrets:
- db_root_password
- db_password
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 30s
timeout: 10s
retries: 5
wordpress:
image: wordpress:6.7-php8.3-apache
restart: unless-stopped
depends_on:
db:
condition: service_healthy
expose:
- "80"
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD_FILE: /run/secrets/db_password
WORDPRESS_DB_NAME: wordpress
volumes:
- wordpress_data:/var/www/html
secrets:
- db_password
volumes:
db_data:
wordpress_data:
secrets:
db_root_password:
file: ./secrets/db_root_password.txt
db_password:
file: ./secrets/db_password.txt
Custom Dockerfile: WordPress with plugins and PHP tuning
FROM wordpress:6.7-php8.3-apache
# Install WP-CLI
RUN curl -o /usr/local/bin/wp \
https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar \
&& chmod +x /usr/local/bin/wp
# PHP upload limits
RUN echo "upload_max_filesize = 64M" \
> /usr/local/etc/php/conf.d/uploads.ini \
&& echo "post_max_size = 64M" \
>> /usr/local/etc/php/conf.d/uploads.ini \
&& echo "memory_limit = 256M" \
>> /usr/local/etc/php/conf.d/uploads.ini
# Copy custom themes and plugins
COPY ./themes/ /usr/src/wordpress/wp-content/themes/
COPY ./plugins/ /usr/src/wordpress/wp-content/plugins/
Docker Compose: WordPress with WP-CLI service
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
wordpress:
image: wordpress:6.7-php8.3-apache
restart: unless-stopped
depends_on:
- db
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: ${MYSQL_USER}
WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD}
WORDPRESS_DB_NAME: ${MYSQL_DATABASE}
volumes:
- wordpress_data:/var/www/html
wpcli:
image: wordpress:cli-php8.3
depends_on:
- db
- wordpress
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: ${MYSQL_USER}
WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD}
WORDPRESS_DB_NAME: ${MYSQL_DATABASE}
volumes:
- wordpress_data:/var/www/html
entrypoint: wp
command: "--info"
user: "33:33"
volumes:
db_data:
wordpress_data:
Bash: WP-CLI management commands
# Install and activate a plugin
docker compose run --rm wpcli plugin install woocommerce --activate
# Update all plugins
docker compose run --rm wpcli plugin update --all
# Search-replace URLs (domain migration)
docker compose run --rm wpcli search-replace \
'http://localhost:8080' 'https://example.com' --all-tables
# Export/import database
docker compose run --rm wpcli db export /var/www/html/backup.sql
docker compose run --rm wpcli db import /var/www/html/backup.sql
# Check status
docker compose run --rm wpcli core version
docker compose run --rm wpcli plugin list
Anti-Patterns
Wrong: Hardcoded passwords in docker-compose.yml
# BAD -- passwords visible in version control
services:
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: my_secret_password
MYSQL_PASSWORD: wordpress123
Correct: Environment variables from .env file
# GOOD -- passwords loaded from .env (excluded from git)
services:
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
# .env file contains actual values, listed in .gitignore
Wrong: No volume persistence
# BAD -- all data lost when container is removed
services:
db:
image: mysql:8.0
# No volumes -- data ephemeral
wordpress:
image: wordpress:latest
# No volumes -- uploads, plugins lost
Correct: Named volumes for all persistent data
# GOOD -- data persists across container recreations
services:
db:
image: mysql:8.0
volumes:
- db_data:/var/lib/mysql
wordpress:
image: wordpress:latest
volumes:
- wordpress_data:/var/www/html
volumes:
db_data:
wordpress_data:
Wrong: Using 'localhost' as database host
# BAD -- WordPress tries to connect to itself
services:
wordpress:
environment:
WORDPRESS_DB_HOST: localhost
Correct: Using Docker service name
# GOOD -- Docker DNS resolves 'db' to MySQL container
services:
wordpress:
environment:
WORDPRESS_DB_HOST: db
Wrong: Exposing MySQL to host in production
# BAD -- MySQL accessible from outside Docker
services:
db:
ports:
- "3306:3306"
Correct: Keep MySQL internal only
# GOOD -- MySQL only reachable by other containers
services:
db:
expose:
- "3306"
Common Pitfalls
- "Error establishing a database connection": WordPress starts before MySQL is ready. Fix: Add
depends_onwithcondition: service_healthyand ahealthcheckon the MySQL service. [src1] - File permission errors on wp-content: WordPress runs as www-data (UID 33) but host volumes may have different ownership. Fix: Use named volumes or run
chown -R 33:33 /var/www/html/wp-content. [src3] - Data disappears after
docker compose down -v: The-vflag removes named volumes. Fix: Never use-vunless you intentionally want to destroy data. [src5] - WP-CLI files owned by root: WP-CLI runs as root by default, creating files WordPress cannot modify. Fix: Add
user: "33:33"to the WP-CLI service. [src1] - Upload size limited to 2MB: PHP defaults restrict uploads. Fix: Mount a custom
php.iniwithupload_max_filesize = 64Mat/usr/local/etc/php/conf.d/uploads.ini. [src4] - WordPress cannot send email: No SMTP configured by default. Fix: Install
wp-mail-smtpplugin and configure SMTP, or add a mail relay container. [src7] - MySQL 8.0 authentication errors: MySQL 8.0 defaults to
caching_sha2_password. Fix: Addcommand: --default-authentication-plugin=mysql_native_passwordto MySQL service. [src5] - Plugin install fails with permission error: WordPress lacks write access to
/var/www/html/wp-content. Fix: Ensure volume is correctly mounted and permissions matchwww-data. [src3]
Diagnostic Commands
# Check running services and health status
docker compose ps
# View logs
docker compose logs wordpress
docker compose logs db
# Test database connectivity from WordPress
docker compose exec wordpress \
php -r "new PDO('mysql:host=db;dbname=wordpress', 'wordpress', getenv('WORDPRESS_DB_PASSWORD'));"
# Check MySQL is accepting connections
docker compose exec db mysqladmin ping -h localhost
# Inspect PHP configuration
docker compose exec wordpress php -i | grep -E 'upload_max|post_max|memory_limit'
# Check disk usage of volumes
docker system df -v | grep -A5 "VOLUME NAME"
# Verify file permissions
docker compose exec wordpress ls -la /var/www/html/wp-content/
# Container resource usage
docker stats --no-stream
Version History & Compatibility
| Component | Version | Status | Notes |
|---|---|---|---|
| WordPress | 6.7 | Current | PHP 8.1-8.3 supported, 8.3 recommended |
| WordPress | 6.6 | Supported | Last version supporting PHP 8.0 |
| MySQL | 8.4 LTS | Current LTS | Supported through 2032, recommended |
| MySQL | 8.0 | EOL April 2026 | Migrate to 8.4 LTS before EOL |
| MariaDB | 10.11 LTS | Current LTS | Supported through Feb 2028 |
| Docker Compose | V2 | Current | Built into Docker CLI |
| Docker Compose | V1 | EOL (June 2023) | Do not use |
| PHP | 8.3 | Current | Default in wordpress:latest |
| phpMyAdmin | 5.x | Current | Supports MySQL 8.x and MariaDB |
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Need a quick local WordPress development environment | Running 10+ high-traffic WordPress sites | Kubernetes with horizontal pod autoscaling |
| Self-hosting WordPress on a single server or VPS | Need a managed WordPress experience | Managed hosting (WP Engine, Kinsta) |
| Need to test plugins/themes in isolation | Need auto-scaling for traffic spikes | Cloud-managed containers (AWS ECS, Cloud Run) |
| Building a reproducible WordPress deployment | Need a static/JAMstack site, not a CMS | Hugo, Next.js, or Astro |
Important Caveats
wordpress:latestupdates automatically -- pin to a specific version (e.g.,wordpress:6.7-php8.3-apache) in production- Docker Compose V1 (
docker-composebinary) reached end-of-life in June 2023 -- use V2 (docker composesubcommand) - MySQL 8.0 reaches end-of-life in April 2026 -- migrate to MySQL 8.4 LTS or MariaDB 10.11 LTS
- WordPress auto-updates may not work in containers with read-only volumes -- manage updates via WP-CLI
- The
_FILEsuffix for Docker secrets is supported by WordPress and MySQL images but NOT by phpMyAdmin - MariaDB uses
MARIADB_*env var prefix since 10.6+ -- the oldMYSQL_*prefix still works but is deprecated