docker-compose.yml with named volumes for data persistence and environment-based configuration.docker compose up -d to start the stack; docker compose exec wordpress wp for WP-CLI./var/lib/mysql and /var/www/html -- without them, docker compose down destroys all your data.| 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 |
| 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) |
| 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) |
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
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.
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.
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.
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.
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.
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.
# 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
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/
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:
# 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
# BAD -- passwords visible in version control
services:
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: my_secret_password
MYSQL_PASSWORD: wordpress123
# 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
# 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
# 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:
# BAD -- WordPress tries to connect to itself
services:
wordpress:
environment:
WORDPRESS_DB_HOST: localhost
# GOOD -- Docker DNS resolves 'db' to MySQL container
services:
wordpress:
environment:
WORDPRESS_DB_HOST: db
# BAD -- MySQL accessible from outside Docker
services:
db:
ports:
- "3306:3306"
# GOOD -- MySQL only reachable by other containers
services:
db:
expose:
- "3306"
depends_on with condition: service_healthy and a healthcheck on the MySQL service. [src1]chown -R 33:33 /var/www/html/wp-content. [src3]docker compose down -v: The -v flag removes named volumes. Fix: Never use -v unless you intentionally want to destroy data. [src5]user: "33:33" to the WP-CLI service. [src1]php.ini with upload_max_filesize = 64M at /usr/local/etc/php/conf.d/uploads.ini. [src4]wp-mail-smtp plugin and configure SMTP, or add a mail relay container. [src7]caching_sha2_password. Fix: Add command: --default-authentication-plugin=mysql_native_password to MySQL service. [src5]/var/www/html/wp-content. Fix: Ensure volume is correctly mounted and permissions match www-data. [src3]# 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
| 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 |
| 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 |
wordpress:latest updates automatically -- pin to a specific version (e.g., wordpress:6.7-php8.3-apache) in productiondocker-compose binary) reached end-of-life in June 2023 -- use V2 (docker compose subcommand)_FILE suffix for Docker secrets is supported by WordPress and MySQL images but NOT by phpMyAdminMARIADB_* env var prefix since 10.6+ -- the old MYSQL_* prefix still works but is deprecated