- 1. Why Docker for WordPress?
- 2. WordPress + Docker Stack Architecture
- 3. Project Directory Structure
- 4. The docker-compose.yml File — Heart of the Stack
- Explaining each part
- 5. Nginx Configuration — Static Files and PHP Proxy
- Key points
- 6. Launch the Stack — Up and Running in 30 Seconds
- 7. Managing WordPress in Docker — Common Commands
- 8. Installing WP-CLI — Manage WordPress from the Command Line
- 9. Production Optimization
- 9.1. Use .env for Secrets
- 9.2. SSL/TLS with Let’s Encrypt
- 9.3. PHP OPcache and Tuning
- 9.4. Automated Backup with Cron
- 10. Comparison: Docker vs Traditional LEMP
- 11. Troubleshooting — Common Errors
- 12. From Docker to Kubernetes — The Next Step
- 13. Conclusion
1. Why Docker for WordPress?
Before touching any code, let us understand what problem Docker solves:
| Traditional Problem | How Docker Solves It | Real-World Benefit |
|---|---|---|
| “It works on my machine” — dev environment differs from production | Containers hold all dependencies: PHP version, extensions, Nginx config | Runs identically on your laptop, staging server, and production |
| Manual installation is time-consuming and error-prone | One docker-compose.yml file defines the entire stack | docker compose up and you are done, under one minute |
| Hard to manage multiple projects with different PHP/MySQL versions | Each project has its own isolated containers | Project A uses PHP 7.4, Project B uses PHP 8.3 — no conflicts |
| Database backup/restore is complex | Docker volumes store data separately, easy to backup with docker exec | Backup is just one SQL file, restore is just one command |
| Scaling is difficult | Run multiple WordPress containers behind a load balancer | Horizontal scaling is easy with Docker Swarm or Kubernetes |
2. WordPress + Docker Stack Architecture
We will build a stack with three containers:
- wordpress:
wordpress:latestimage — runs PHP-FPM, holds WordPress source code - db:
mysql:8.0image — MySQL database for WordPress - nginx:
nginx:alpineimage — reverse proxy, handles static files, forwards PHP requests to the WordPress container
Client →
Nginx (port 80) →
WordPress PHP-FPM (port 9000) →
MySQL (port 3306)
3. Project Directory Structure
Create a project directory with this structure:
wordpress-docker/
├── docker-compose.yml # Defines the entire stack
├── nginx/
│ └── default.conf # Nginx configuration
├── wordpress/ # WordPress source (mounted from container)
└── db_data/ # MySQL data (Docker volume)4. The docker-compose.yml File — Heart of the Stack
This is the most important file. It defines three services, a shared network, and volumes for data persistence:
version: '3.8'
services:
db:
image: mysql:8.0
container_name: wp_db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: rootpassword123
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: wordpresspass
volumes:
- db_data:/var/lib/mysql
networks:
- wp_network
command: --default-authentication-plugin=mysql_native_password
wordpress:
image: wordpress:php8.3-fpm
container_name: wp_app
restart: unless-stopped
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_NAME: wordpress
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: wordpresspass
volumes:
- ./wordpress:/var/www/html
networks:
- wp_network
depends_on:
- db
nginx:
image: nginx:alpine
container_name: wp_nginx
restart: unless-stopped
ports:
- "80:80"
volumes:
- ./wordpress:/var/www/html
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
networks:
- wp_network
depends_on:
- wordpress
volumes:
db_data:
networks:
wp_network:
driver: bridgeExplaining each part:
restart: unless-stopped— Container auto-restarts when the server reboots, unless you manually stop it.volumes—db_datais a named volume, Docker manages it and data persists even when the container is deleted../wordpressis a bind mount, allowing you to edit files from the host.networks— Containers in the same network can reach each other by service name. WordPress connects to MySQL viadb:3306.depends_on— Ensures MySQL starts before WordPress, avoiding connection refused errors.
5. Nginx Configuration — Static Files and PHP Proxy
Create nginx/default.conf:
server {
listen 80;
server_name localhost;
root /var/www/html;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
fastcgi_pass wordpress:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 6M;
add_header Cache-Control "public, immutable";
}
location ~ /\.ht {
deny all;
}
}Key points:
fastcgi_pass wordpress:9000— Nginx sends PHP requests to the WordPress container on port 9000 (PHP-FPM). Docker DNS automatically resolveswordpressto the container IP.try_files— Supports WordPress permalinks. If the file does not exist, fall back toindex.php.expires 6M— Cache static assets for 6 months, reducing server load.
6. Launch the Stack — Up and Running in 30 Seconds
Open a terminal, cd into the project directory, and run:
# 1. Create directories
mkdir -p wordpress-docker/nginx wordpress-docker/wordpress
# 2. Create docker-compose.yml and nginx/default.conf (copy from above)
# 3. Launch
cd wordpress-docker
docker compose up -d
# 4. Check running containers
docker compose ps
# 5. View logs if needed
docker compose logs -fAfter running, visit http://localhost and you will see the WordPress setup screen. Complete the setup as usual.
7. Managing WordPress in Docker — Common Commands
| Operation | Command |
|---|---|
| List containers | docker compose ps |
| View real-time logs | docker compose logs -f |
| Access WordPress container shell | docker exec -it wp_app bash |
| Run WP-CLI | docker exec -it wp_app wp --allow-root [command] |
| Backup database | docker exec wp_db mysqldump -u wordpress -pwordpresspass wordpress > backup.sql |
| Restore database | docker exec -i wp_db mysql -u wordpress -pwordpresspass wordpress < backup.sql |
| Restart a service | docker compose restart nginx |
| Rebuild after config changes | docker compose up -d --build |
| Stop the entire stack | docker compose down |
| Stop and delete volumes (careful) | docker compose down -v |
8. Installing WP-CLI — Manage WordPress from the Command Line
Instead of using wp-admin, you can install plugins, update core, and create users with WP-CLI:
# Install WP-CLI into the WordPress container
docker exec -it wp_app bash -c "
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
mv wp-cli.phar /usr/local/bin/wp
"
# Check WP-CLI
docker exec -it wp_app wp --allow-root --info
# Install plugin
docker exec -it wp_app wp --allow-root plugin install wordpress-seo --activate
# Create admin user
docker exec -it wp_app wp --allow-root user create admin [email protected] --role=administrator --user_pass=admin123
# Update WordPress core
docker exec -it wp_app wp --allow-root core update9. Production Optimization
Development and production differ in several important ways:
9.1. Use .env for Secrets
Never hardcode passwords in docker-compose.yml. Create a .env file:
MYSQL_ROOT_PASSWORD=your_secure_root_password
MYSQL_DATABASE=wordpress
MYSQL_USER=wordpress
MYSQL_PASSWORD=your_secure_db_password
WORDPRESS_DB_PASSWORD=your_secure_db_passwordUpdate docker-compose.yml:
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}Add .env to .gitignore so it does not get pushed to Git.
9.2. SSL/TLS with Let’s Encrypt
Add a certbot container for automatic SSL renewal:
certbot:
image: certbot/certbot
container_name: wp_certbot
volumes:
- ./certbot/conf:/etc/letsencrypt
- ./certbot/www:/var/www/certbot
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait ${!}; done;'"
nginx:
# ... existing config
volumes:
- ./wordpress:/var/www/html
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
- ./certbot/conf:/etc/letsencrypt
- ./certbot/www:/var/www/certbotUpdate nginx/default.conf to redirect HTTP to HTTPS and use certificates:
server {
listen 80;
server_name yourdomain.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
root /var/www/html;
index index.php;
# ... rest of config same as before
}9.3. PHP OPcache and Tuning
Create a custom php.ini and mount it into the WordPress container:
; php.ini
memory_limit = 256M
upload_max_filesize = 64M
post_max_size = 64M
max_execution_time = 300
opcache.enable = 1
opcache.memory_consumption = 128
opcache.interned_strings_buffer = 8
opcache.max_accelerated_files = 4000
opcache.revalidate_freq = 2
opcache.fast_shutdown = 1Add to docker-compose.yml:
wordpress:
# ...
volumes:
- ./wordpress:/var/www/html
- ./php.ini:/usr/local/etc/php/conf.d/custom.ini9.4. Automated Backup with Cron
Create a daily backup script:
#!/bin/bash
# backup.sh
DATE=$(date +%Y%m%d_%H%M%S)
docker exec wp_db mysqldump -u wordpress -pwordpresspass wordpress | gzip > ./backups/wordpress_$DATE.sql.gz
find ./backups -name "*.sql.gz" -mtime +7 -deleteAdd to crontab:
0 2 * * * /path/to/wordpress-docker/backup.sh10. Comparison: Docker vs Traditional LEMP
| Criteria | Manual LEMP | Docker |
|---|---|---|
| Setup time | 30-60 minutes (install each component, configure each file) | 1-2 minutes (docker compose up -d) |
| Reproducibility | Low — each server may differ due to manual configuration | High — same docker-compose.yml runs identically everywhere |
| Version management | Hard — downgrading PHP/MySQL is complex | Easy — change image tag, restart container |
| Multi-project | Hard — port conflicts, PHP version clashes | Easy — each project has its own isolated container stack |
| Resource usage | Lower (runs directly on host) | Slightly higher (container overhead ~1-5%) |
| Learning curve | Low — just need to know Linux + Nginx + MySQL | Medium — need to understand containers, volumes, networks |
| Production-ready | Requires significant manual optimization effort | Easy to scale, easy to backup, easy to CI/CD |
11. Troubleshooting — Common Errors
| Error | Cause | Fix |
|---|---|---|
Error establishing database connection | WordPress cannot connect to MySQL yet | Wait a few more seconds for MySQL to finish starting, or run docker compose restart wordpress |
502 Bad Gateway | Nginx cannot connect to PHP-FPM | Check fastcgi_pass wordpress:9000 in nginx config. Ensure the WordPress container is running. |
| Port 80 already in use | Another Apache/Nginx is running on the host | Stop the service using the port: sudo systemctl stop nginx or change port mapping to 8080:80 |
| Permission denied when editing WordPress files | Files in the container belong to www-data user | Run sudo chown -R $USER:$USER ./wordpress on the host |
Database lost after docker compose down | Used -v flag or did not use a named volume | Ensure you use the named volume db_data, and do not run down -v unless you really want to delete data |
12. From Docker to Kubernetes — The Next Step
As your site grows, you might need:
- Docker Swarm — simple orchestration, built into Docker. Suitable for 1-3 servers.
- Kubernetes — powerful orchestration, auto-scaling, self-healing. Suitable for large systems, multi-cloud.
- Docker + Cloud — AWS ECS, Google Cloud Run, Azure Container Instances all support running WordPress containers without managing servers.
But do not rush. For most WordPress sites, a single server with Docker Compose is more than enough. Only consider Kubernetes when you truly need horizontal scaling or running across multiple availability zones.
13. Conclusion
Docker is not a “magic” technology — it is just a tool. But for WordPress, Docker solves exactly the most painful problems: inconsistent environments, repetitive setup, complex backup/restore.
With just two files — docker-compose.yml and nginx/default.conf — you have a production-ready WordPress stack that can run on any machine with Docker, from your personal laptop to a cloud VPS.
Try copying the config above, run docker compose up -d, and experience the feeling of “everything just works” — that is exactly why Docker has become the industry standard for application deployment.

Comments