Docker Compose provides a reproducible local WordPress development environment that eliminates the “works on my machine” problem — a docker-compose.yml file checked into the project repository defines the complete stack (WordPress, Nginx or Apache, MySQL, Redis, phpMyAdmin) with pinned versions, eliminating installation differences between team members and ensuring that the local environment matches the production server configuration. Unlike XAMPP or MAMP, Docker containers are isolated, allow multiple projects to run simultaneously on different ports without conflicts, and can be recreated from scratch in seconds when the environment becomes corrupted. The fundamental WordPress Docker Compose stack consists of four services: mysql (the database, using the official mysql:8.0 image with a named volume for data persistence), wordpress (the PHP-FPM application, using wordpress:6.4-php8.2-fpm), nginx (the web server with a custom nginx.conf that proxies .php requests to the wordpress FPM container), and optionally redis (redis:7-alpine) and phpmyadmin. The WordPress theme and plugin code under development is bind-mounted from the local filesystem into the container’s /var/www/html/wp-content/ directory — changes to local files appear instantly in the container without rebuilding the image. A .env file stores environment variables (database credentials, site URL) so they are not hardcoded in docker-compose.yml and are not committed to version control. WordPress environment variables (WORDPRESS_DB_HOST, WORDPRESS_DB_USER, WORDPRESS_TABLE_PREFIX) configure wp-config.php automatically via the Docker entrypoint script — no manual wp-config.php editing is needed. The Git worktrees post covered parallel branch development in a shared environment; Docker Compose isolates each project in its own containerized stack, enabling multiple projects to run concurrently without port conflicts.
Problem: A development team of 4 has three different local setups (XAMPP, Valet, wp-env) — onboarding a new developer takes 3 hours of environment setup, database configuration files differ between developers causing git conflicts in wp-config.php, and the local PHP version (7.4 on one machine) causes bugs that only appear in production (PHP 8.2).
Solution: Replace all local setups with a docker-compose.yml in the project root — any developer runs docker compose up -d and has a running WordPress environment with PHP 8.2, MySQL 8, and Redis in under 2 minutes.
# docker-compose.yml
version: "3.9"
services:
mysql:
image: mysql:8.0
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpass}
MYSQL_DATABASE: ${MYSQL_DATABASE:-wordpress}
MYSQL_USER: ${MYSQL_USER:-wpuser}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-wppass}
volumes:
- mysql_data:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
wordpress:
image: wordpress:6.4-php8.2-fpm-alpine
restart: unless-stopped
depends_on:
mysql:
condition: service_healthy
environment:
WORDPRESS_DB_HOST: mysql:3306
WORDPRESS_DB_NAME: ${MYSQL_DATABASE:-wordpress}
WORDPRESS_DB_USER: ${MYSQL_USER:-wpuser}
WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD:-wppass}
WORDPRESS_TABLE_PREFIX: wp_
WORDPRESS_DEBUG: "true"
WORDPRESS_CONFIG_EXTRA: |
define('WP_REDIS_HOST', 'redis');
define('WP_HOME', 'http://localhost:8080');
define('WP_SITEURL', 'http://localhost:8080');
volumes:
# Bind-mount theme and plugin directories for live editing
- ./wp-content/themes/my-theme:/var/www/html/wp-content/themes/my-theme
- ./wp-content/plugins/my-plugin:/var/www/html/wp-content/plugins/my-plugin
- wordpress_data:/var/www/html
nginx:
image: nginx:1.25-alpine
restart: unless-stopped
ports:
- "8080:80"
depends_on:
- wordpress
volumes:
- wordpress_data:/var/www/html:ro
- ./docker/nginx.conf:/etc/nginx/conf.d/default.conf:ro
redis:
image: redis:7-alpine
restart: unless-stopped
phpmyadmin:
image: phpmyadmin:5.2
restart: unless-stopped
ports:
- "8081:80"
environment:
PMA_HOST: mysql
PMA_USER: ${MYSQL_USER:-wpuser}
PMA_PASSWORD: ${MYSQL_PASSWORD:-wppass}
volumes:
mysql_data:
wordpress_data:
# docker/nginx.conf
server {
listen 80;
server_name localhost;
root /var/www/html;
index index.php;
# WordPress pretty permalinks
location / {
try_files $uri $uri/ /index.php?$args;
}
# Pass PHP to wordpress FPM container
location ~ \.php$ {
fastcgi_pass wordpress:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
# Deny direct access to sensitive files
location ~* /(?:wp-config\.php|\.git|\.env) {
deny all;
}
}
NOTE: The wordpress_data volume persists the full WordPress installation (core files, uploads, all plugins and themes) across container restarts — this prevents WordPress from re-downloading core files every time the container restarts. However, it also means that docker compose down -v (which deletes named volumes) will destroy all uploads and any plugins installed via the wp-admin UI rather than bind-mounted. Use docker compose down (without -v) for normal stop/start operations, and reserve -v for a full environment reset. Add ./wp-content/uploads:/var/www/html/wp-content/uploads as an additional bind mount if you want uploads preserved in the project directory and backed up with git-lfs or a dedicated backup solution.