So, you want to run WordPress in Docker, and better yet, have that setup be pretty similar whether you’re developing locally or deploying to production? Great idea! The main takeaway upfront is that Docker and Docker Compose can indeed give you excellent local/production parity for WordPress, provided you set things up thoughtfully. This means using a multi-stage Dockerfile, externalizing configurations, and treating your database and uploads as persistent, separate concerns.
Before we dive into the “how,” let’s quickly touch on the “why.” You might be thinking, “WordPress is easy to install, why add the complexity of Docker?” And you’re right, a basic LAMP stack is straightforward. However, Docker brings some significant advantages:
Consistent Environments
This is the big one. “It works on my machine” becomes “It works in this container.” No more wrestling with differing PHP versions, missing extensions, or database quirks between your development laptop, your team member’s setup, and your production server. Everyone gets the exact same environment bundled into an image.
Simplified Setup for Developers
New team member? Just docker-compose up. They don’t need to manually install Apache, Nginx, PHP, MySQL, or worry about dependencies. It’s all defined and packaged.
Isolation and Resource Management
Each component (Nginx, PHP-FPM, MySQL) runs in its own container. This isolates issues – a problem in your PHP doesn’t tank your database. It also makes resource management clearer.
Easier Scaling (for Production)
While for a single WordPress site, this might be overkill, understanding how to containerize lays the groundwork for scaling in more complex environments using orchestrators like Kubernetes or Swarm.
Version Control for Infrastructure
Your Dockerfile and docker-compose.yml become part of your code repository. This means your infrastructure definitions are versioned, auditable, and repeatable.
If you’re looking to enhance your WordPress deployment strategy, you might find it beneficial to explore related topics such as server migrations. A relevant article that discusses the process of migrating from one CyberPanel server to another can provide valuable insights into managing your WordPress environment effectively. You can read more about it in this article: Migrating to Another CyberPanel Server. This information can be particularly useful when considering how to maintain consistency across different environments, whether local or production.
Crafting Your Docker Setup: The Core Components
To containerize WordPress, we’ll typically be working with several interconnected containers. Think of it like building a small apartment complex for your website.
The WordPress Application Container (PHP & Web Server)
This is where your WordPress files and PHP code live. We’ll use a PHP-FPM image and often pair it with a separate Nginx container for serving requests. While you could run Apache directly in the PHP container, it’s generally cleaner and more performant to separate PHP-FPM and Nginx.
The Database Container
WordPress needs a database, usually MySQL or MariaDB. This will be a separate container, totally independent of your WordPress application. This separation is crucial for persistence and scaling.
The Reverse Proxy / Web Server Container (Nginx)
Nginx acts as the front door to your WordPress site. It handles incoming HTTP requests, serves static files efficiently, and passes dynamic PHP requests to our PHP-FPM container. This setup is common and generally recommended for performance and flexibility.
Setting Up Your Local Development Environment
Let’s start with a solid local setup using Docker Compose. This will be the foundation for our production parity.
The docker-compose.yml File
This is the blueprint for your multi-container application. We’ll define all our services here.
“`yaml
version: ‘3.8’
services:
nginx:
image: nginx:stable-alpine
container_name: wordpress_nginx
ports:
- “80:80”
volumes:
- ./nginx/nginx.conf:/etc/nginx/conf.d/default.conf
- ./src:/var/www/html # Mount your WordPress source code
depends_on:
- wordpress
networks:
- wordpress_network
wordpress:
build:
context: . # Points to the directory containing the Dockerfile
dockerfile: Dockerfile.prod # We’ll reuse our production Dockerfile here
container_name: wordpress_app
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: ${MYSQL_USER}
WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD}
WORDPRESS_DB_NAME: ${MYSQL_DATABASE}
WORDPRESS_TABLE_PREFIX: wp_ # Good practice to customize
volumes:
- ./src:/var/www/html # Mount your WordPress source code into the container
- uploads:/var/www/html/wp-content/uploads # Dedicate a volume for uploads
depends_on:
- db
restart: always # Keep it running
networks:
- wordpress_network
db:
image: mariadb:10.6 # Or mysql:8.0, choose your preferred DB
container_name: wordpress_db
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} # Root for initial setup
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- db_data:/var/lib/mysql # Persistent volume for database data
restart: always
networks:
- wordpress_network
volumes:
db_data: # Declaring the named volume for database persistence
uploads: # Declaring the named volume for WordPress uploads persistence
networks:
wordpress_network:
driver: bridge
“`
The Nginx Configuration
Create a directory named nginx and inside it, a file nginx.conf:
“`nginx
server {
listen 80;
server_name localhost; # Or your local domain
root /var/www/html;
index index.php index.html index.htm;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass wordpress:9000; # ‘wordpress’ is the service name in docker-compose
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
}
Deny access to hidden files
location ~ /\. {
deny all;
}
Deny access to SQL files, etc.
location ~* \.(sql|md|log|sh|bak|zip|rar|tar\.gz)$ {
deny all;
}
Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires max;
log_not_found off;
}
}
“`
The Multi-Stage Dockerfile for WordPress
This file will define how your WordPress application image is built. Using multi-stage builds is key for smaller and more secure production images. Create a file named Dockerfile.prod in your root directory.
“`dockerfile
Stage 1: Build dependencies and composer packages (if you use them)
FROM composer:2.6 as composer_build
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install –no-dev –no-plugins –no-scripts –prefer-dist –optimize-autoloader –no-progress –ansi
Stage 2: Install WordPress CLI
FROM wodby/wpcli:2.8.1 as wpcli
WORKDIR /usr/local/bin
RUN curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar \
&& chmod +x wp-cli.phar \
&& mv wp-cli.phar wp
Stage 3: The actual WordPress application image
FROM wordpress:6.4.3-php8.2-fpm-alpine # Use a specific, stable tag
Set working directory inside the container
WORKDIR /var/www/html
Copy the built dependencies from stage 1 (if any)
COPY –from=composer_build /app/vendor /var/www/html/vendor
Copy wp-cli from stage 2
COPY –from=wpcli /usr/local/bin/wp /usr/local/bin/wp
Copy your local WordPress files into the container
For local dev, you’ll volume mount this. For production, you’ll copy.
We’ll keep this commented for the local dev docker-compose.yml to rely on volume mount.
But for a true production build (e.g., using a CI/CD pipeline), you’d uncomment this.
COPY . /var/www/html/
Handle permissions
The default user for wordpress:fpm-alpine is www-data (UID 82).
We might need to adjust ownership based on your local user mapping for shared volumes,
but for production, this should generally be fine, or you might need a dedicated entrypoint script.
USER www-data
Expose port (PHP-FPM usually listens on 9000)
EXPOSE 9000
CMD [“php-fpm”]
“`
Important note on Dockerfile.prod: For local development, we’re mounting your src directory into the container. This means changes you make locally are immediately reflected. For a true production build, you would typically uncomment COPY . /var/www/html/ in the Dockerfile.prod and build that image in your CI/CD pipeline, omitting runtime volume mounts for the core WordPress files (only for uploads and potentially themes/plugins for specific setups). This ensures a completely self-contained, immutable image.
Environment Variables (.env file)
Create a .env file in the same directory as your docker-compose.yml. This keeps sensitive information out of your version control.
“`
MYSQL_ROOT_PASSWORD=your_super_secret_root_password
MYSQL_DATABASE=wordpress
MYSQL_USER=wp_user
MYSQL_PASSWORD=your_wp_db_password
“`
Initializing WordPress
Once all these files are in place, navigate to your project root in your terminal and run:
“`bash
docker-compose up -d
“`
This will build your images (if not cached), pull official ones, and start all your services.
You should now be able to access your fresh WordPress installation at http://localhost. Run through the standard WordPress 5-minute install.
Achieving Production Parity
Now, how do we make this local setup production-ready and ensure parity? The main differences boil down to how images are built, how volumes are managed, and sometimes, network configuration.
Building Production-Ready Images
For production, you generally want to COPY your entire application code into the image (excluding sensitive files via .dockerignore) rather than volume-mounting it at runtime. This creates immutable images.
Production Dockerfile Best Practices
- Specific Base Images: Always use specific tags (e.g.,
wordpress:6.4.3-php8.2-fpm-alpine) instead oflatest. - Multi-Stage Builds: As shown above, this ensures your final image is small and only contains necessary runtime components, not build tools.
- No Dev Dependencies: Use
composer install --no-devif you’re using Composer. - Security: Run as a non-root user (the WordPress FPM images often handle this by default with
www-data). Minimize unnecessary software. - Caching: Leverage Docker’s build cache by ordering instructions from least-to-most frequently changing.
Externalizing Configurations for Production
Never hardcode secrets or environment-specific values directly into your Dockerfile or docker-compose.yml for production.
Using Environment Variables
For production, you’ll still use environment variables, but they’ll be managed differently. Instead of a .env file, you might use:
- Orchestrator Secrets: Kubernetes secrets, Docker Swarm secrets.
- Cloud Provider Secrets Management: AWS Secrets Manager, Google Secret Manager, Azure Key Vault.
- Direct Environment Variables: Set during deployment in your CI/CD pipeline or directly on the host/container orchestration.
Persistent Data: Volumes and Backups
Database and user uploads are your persistent data. They must survive container restarts and even container deletion.
Named Volumes vs. Bind Mounts
- Local Development: Bind mounts are great (
./src:/var/www/html) because changes on your host are immediately reflected in the container. - Production: Named volumes (
db_data:/var/lib/mysql) are preferred for performance, data isolation, and management by the Docker daemon. For uploads, you might also use cloud storage (S3, GCS) via a plugin for better scalability and durability.
Database Backup Strategy
Ensure your production database has a robust backup and restore strategy. This is outside Docker’s direct scope but critical for production. Think automated nightly backups to a different location.
Handling wp-config.php
This file is a tricky one for parity. It contains database credentials, salts, and often other environment-specific configurations.
Dynamic wp-config.php
You can create a wp-config-docker.php that pulls variables from the environment and then includes the standard wp-config.php at the very end. The official WordPress Docker images often handle this by default if you use their environment variables. For custom setups, an entrypoint script can generate or modify wp-config.php dynamically on container start.
“`php
// wp-config.php (or an entrypoint script that generates or modifies it)
// Pull environment variables for database
define(‘DB_NAME’, getenv(‘WORDPRESS_DB_NAME’));
define(‘DB_USER’, getenv(‘WORDPRESS_DB_USER’));
define(‘DB_PASSWORD’, getenv(‘WORDPRESS_DB_PASSWORD’));
define(‘DB_HOST’, getenv(‘WORDPRESS_DB_HOST’));
define(‘DB_CHARSET’, ‘utf8’);
define(‘DB_COLLATE’, ”);
// Unique salts (generate these securely for production!)
define(‘AUTH_KEY’, getenv(‘AUTH_KEY’));
define(‘SECURE_AUTH_KEY’, getenv(‘SECURE_AUTH_KEY’));
define(‘LOGGED_IN_KEY’, getenv(‘LOGGED_IN_KEY’));
define(‘NONCE_KEY’, getenv(‘NONCE_KEY’));
define(‘AUTH_SALT’, getenv(‘AUTH_SALT’));
define(‘SECURE_AUTH_SALT’, getenv(‘SECURE_AUTH_SALT’));
define(‘LOGGED_IN_SALT’, getenv(‘LOGGED_IN_SALT’));
define(‘NONCE_SALT’, getenv(‘NONCE_SALT’));
$table_prefix = getenv(‘WORDPRESS_TABLE_PREFIX’) ? getenv(‘WORDPRESS_TABLE_PREFIX’) : ‘wp_’;
// Example for dynamic URL (useful for development vs. production)
if (getenv(‘WORDPRESS_URL’)) {
define(‘WP_HOME’, getenv(‘WORDPRESS_URL’));
define(‘WP_SITEURL’, getenv(‘WORDPRESS_URL’));
}
// Memory limit (optional, can be in php.ini)
define(‘WP_MEMORY_LIMIT’, ‘256M’);
// Debug mode (set to false for production!)
define(‘WP_DEBUG’, getenv(‘WORDPRESS_DEBUG’) === ‘true’ ? true : false);
/ That’s all, stop editing! Happy publishing. /
/** Absolute path to the WordPress directory. */
if ( !defined(‘ABSPATH’) )
define(‘ABSPATH’, __DIR__ . ‘/’);
/** Sets up WordPress vars and included files. */
require_once ABSPATH . ‘wp-settings.php’;
“`
You’d then add these environment variables to your .env file (local) or your deployment environment (production).
Dealing with Themes, Plugins, and Uploads
How you handle these can significantly impact your workflow and production parity.
Core WordPress Files & Standard Plugins/Themes
For production, these are ideally identical across environments. WordPress core files, and any pre-selected plugins/themes (not client-specific ones that might change frequently) should be baked into your Docker image. This means they are present when the container starts.
Custom Themes and Plugins
If you have custom themes or plugins that are part of your application’s source code, they should be included in the COPY . /var/www/html/ step of your production Dockerfile.
User-Installed Plugins/Themes and Uploads
These are usually written to by the WordPress application.
- Uploads (
wp-content/uploads): Always use a named volume for persistence, or even better for production, integrate with cloud storage (AWS S3, Google Cloud Storage) using a plugin like WP Offload Media. This decouples uploads from your server and provides better scalability and durability. - Plugins/Themes installed via WP Admin: This is generally discouraged for containerized environments. If a user installs a plugin via the admin, it’s written inside that specific container instance. If the container restarts or a new instance spins up, that change is lost. The ideal approach is to manage plugins and themes via your build process (Composer, Git submodules) and bake them into the image. If you absolutely must allow in-admin installs, you’d need a persistent volume for
wp-contentbut this comes with complexities around image immutability and rebuilds.
If you’re looking to enhance your understanding of containerization, you might find it helpful to explore a related article that delves into the best practices for deploying applications using Docker. This resource provides valuable insights that can complement your knowledge on how to containerize WordPress with Docker and Docker Compose for local and production parity. For more information, check out this informative piece at The Sheryar.
Deploying to Production
The principles we’ve discussed apply, but the tooling changes.
Orchestration Tools (Docker Compose, Swarm, Kubernetes)
- Docker Compose: Excellent for single-server deployments (e.g., a simple VPS). Your
docker-compose.ymlcan be mostly reused, with environmental variables externalized as discussed. - Docker Swarm: For slightly larger, but still relatively simple, multi-server deployments. Similar concepts to Compose.
- Kubernetes: The industry standard for complex, scalable, distributed applications. Here,
docker-compose.ymltranslates into Kubernetes manifests (Deployments, Services, PersistentVolumeClaims, Secrets, ConfigMaps). This has a steeper learning curve but offers immense power.
CI/CD Pipeline
For production, you’ll want an automated process:
- Code Commit: Push your changes (WordPress code, Dockerfiles, Nginx configs) to Git.
- Build Image: Your CI pipeline builds the Docker image(s) for your WordPress application and Nginx.
- Tag and Push: Tag the images with a unique version (e.g., Git commit hash) and push them to a Docker Registry (Docker Hub, AWS ECR, GCR).
- Deployment: Your CD pipeline pulls the new images and updates your running containers on your production server(s) using your chosen orchestrator.
If you’re looking to enhance the performance of your containerized WordPress site, you might find it beneficial to explore optimization techniques. A related article that can help you understand how to improve your website’s speed is available at Google PageSpeed Insights. This resource provides valuable insights into optimizing your site for better performance, which can be particularly useful when working with Docker and Docker Compose for both local and production environments.
Final Thoughts on Parity and Maintenance
Achieving true 100% production parity is often an aspirational goal, not always fully attainable or even necessary. The key is sufficient parity to prevent “works on my machine” issues.
Iteration and Refinement
Your Docker setup will evolve. Start simple, then add complexity as needed. Don’t over-engineer from day one.
Monitoring and Logging
Ensure you have proper monitoring for your containers (CPU, memory, network) and centralized logging. Containers often write logs to stdout/stderr, which can be collected by your orchestration platform or a dedicated logging solution.
Security Updates
Regularly update your base images (wordpress, nginx, mariadb). Baking these into your CI/CD process ensures you’re always running secure, up-to-date environments.
By following these guidelines, you’ll create a robust, consistent, and maintainable WordPress environment using Docker, bridging the gap between your local development and your production deployments.