Orchestrating Microservices with Docker Swarm: A Scalable and Resilient WordPress Headless Architecture
Docker Swarm for Headless WordPress: Core Concepts
Deploying a headless WordPress architecture demands robust orchestration for scalability and resilience. Docker Swarm offers a native, integrated solution for container orchestration that is often overlooked in favor of more complex platforms like Kubernetes. For many use cases, Swarm provides a simpler, yet powerful, alternative. This post details how to set up and manage a headless WordPress stack using Docker Swarm, focusing on the database, WordPress core, and a reverse proxy/API gateway.
Our architecture will consist of three primary services:
- Database: A highly available MySQL cluster (or MariaDB).
- WordPress Core: The PHP-FPM and Nginx/Apache components serving the WordPress REST API and admin interface.
- Reverse Proxy/API Gateway: Nginx or Traefik to handle SSL termination, routing, and potentially rate limiting or authentication for API requests.
We’ll leverage Docker Swarm’s built-in features like service discovery, load balancing, rolling updates, and secrets management.
Setting Up the Swarm Cluster
Before deploying services, you need a Docker Swarm cluster. This can be a single manager node for testing or multiple manager and worker nodes for production. Initialize the swarm on your manager node:
On the manager node:
docker swarm init --advertise-addr
This command will output a `docker swarm join` command. Run this on your worker nodes to add them to the swarm.
To verify your nodes:
docker node ls
Database Layer: Highly Available MySQL/MariaDB
For production, a single database instance is a single point of failure. We’ll use a multi-node MySQL/MariaDB setup orchestrated by Docker Swarm. A common pattern is to use a replication setup (primary-replica) or a Galera Cluster for synchronous multi-master replication. For simplicity and common use cases, we’ll outline a primary-replica setup with a proxy for read/write splitting.
We’ll define a Docker Compose file (which Swarm understands) to manage these services.
MariaDB Primary-Replica with ProxySQL
ProxySQL is an intelligent proxy for MySQL that can handle load balancing, query routing, and failover. We’ll deploy it alongside our MariaDB instances.
Create a file named docker-compose.yml:
[version: '3.7']
services:
db-primary:
image: mariadb:10.6
command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
restart: always
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress_user
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- db_primary_data:/var/lib/mysql
networks:
- app-network
deploy:
replicas: 1
placement:
constraints:
- node.role == manager # Or a dedicated DB node role
db-replica:
image: mariadb:10.6
command: --transaction-isolation=READ-COMMITTED --binlog-format=ROW
restart: always
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
volumes:
- db_replica_data:/var/lib/mysql
networks:
- app-network
depends_on:
- db-primary
deploy:
replicas: 2 # Deploy two replicas for HA
placement:
constraints:
- node.role == worker # Or a dedicated DB node role
db-proxy:
image: proxysql/proxysql:2.3.1
restart: always
ports:
- "6033:6033" # MySQL client port
- "7070:7070" # ProxySQL admin interface
- "9090:9090" # ProxySQL stats interface
environment:
MYSQL_HOST: db-primary # Initial host for ProxySQL config
MYSQL_PORT: 3306
MYSQL_USER: wordpress_user
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
volumes:
- proxysql_data:/etc/proxysql
networks:
- app-network
depends_on:
- db-primary
- db-replica
deploy:
replicas: 2 # HA for the proxy
placement:
constraints:
- node.role == manager # Or a dedicated proxy node role
volumes:
db_primary_data:
db_replica_data:
proxysql_data:
networks:
app-network:
driver: overlay
Explanation:
db-primary: A single MariaDB instance. In a real production setup, you’d configure replication from this primary to the replicas. For simplicity here, we’re assuming manual or external configuration of replication, or you might opt for a Galera cluster image.db-replica: Two instances of MariaDB. These would be configured as replicas ofdb-primary. Docker Swarm will ensure these services are running.db-proxy: ProxySQL. It’s configured to connect to the database nodes. We’ll need to further configure ProxySQL to manage the primary/replica setup and routing.volumes: Using named volumes for persistent data.networks: An overlay network for inter-container communication across nodes.deploy.replicas: Specifies the desired number of instances for each service. Swarm will maintain this count.deploy.placement.constraints: Ensures services are scheduled on specific node types (e.g., managers for critical services, workers for others).
Note on Replication: The above configuration doesn’t automatically set up MariaDB replication. You would typically achieve this by:
- Using a MariaDB image that supports Galera Cluster (e.g.,
mariadb:10.6-galera) and configuring it appropriately. - Manually configuring replication after the containers are up, or using an entrypoint script that sets up replication.
- Using a dedicated database orchestration tool.
For ProxySQL configuration, you’d typically mount a custom proxysql.cnf file or use environment variables to bootstrap its configuration. This involves defining hostgroups, users, and query rules for read/write splitting.
To deploy this stack:
# Set your environment variables (e.g., in a .env file or directly) export MYSQL_ROOT_PASSWORD='your_strong_root_password' export MYSQL_PASSWORD='your_strong_db_password' # Deploy the stack docker stack deploy -c docker-compose.yml wordpress_db
Check the status:
docker stack services wordpress_db docker service ps wordpress_db_db-primary
WordPress Core Service
The WordPress core service will consist of PHP-FPM and a web server (Nginx in this example) to serve the REST API and the admin dashboard. We’ll also need to mount the WordPress files.
WordPress Files Management
For headless WordPress, the actual WordPress core files (PHP, themes, plugins) need to be accessible by the PHP-FPM container. Several strategies exist:
- Shared Volume: Mount a named volume or host directory containing WordPress files. This is simple but can lead to synchronization issues if not managed carefully.
- Build-time Integration: Bake WordPress core, themes, and plugins into a custom Docker image. This is the most robust approach for production.
- External File System: Use a distributed file system (e.g., NFS, GlusterFS) mounted on all nodes.
We’ll assume a custom Docker image for WordPress core for this example, as it’s best practice. This image would be built from a Dockerfile that includes WordPress, necessary PHP extensions, and your custom themes/plugins.
WordPress Dockerfile Example
Create a file named Dockerfile.wordpress:
# Use an official PHP-FPM image with Apache or Nginx
FROM php:8.1-fpm
# Install necessary extensions for WordPress
RUN apt-get update && apt-get install -y \
libzip-dev \
libpng-dev \
libjpeg-dev \
libfreetype6-dev \
libssl-dev \
libwebp-dev \
git \
unzip \
&& rm -rf /var/lib/apt/lists/* \
&& docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp \
&& docker-php-ext-install -j$(nproc) gd zip exif pcntl sockets \
&& docker-php-ext-enable gd zip exif pcntl sockets
# Install Composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
# Download WordPress core
RUN curl -o wordpress.tar.gz -SL https://wordpress.org/latest.tar.gz \
&& tar -xzf wordpress.tar.gz -C /var/www/html --strip-components=1 \
&& rm wordpress.tar.gz
# Install plugins and themes (example)
# COPY --chown=www-data:www-data ./wp-content/plugins/ /var/www/html/wp-content/plugins/
# COPY --chown=www-data:www-data ./wp-content/themes/ /var/www/html/wp-content/themes/
# Set ownership for WordPress files
RUN chown -R www-data:www-data /var/www/html
# Expose the PHP-FPM port
EXPOSE 9000
# Set the working directory
WORKDIR /var/www/html
# Default command to run PHP-FPM
CMD ["php-fpm"]
Build this image:
docker build -t your-dockerhub-username/wordpress-core:latest -f Dockerfile.wordpress . docker push your-dockerhub-username/wordpress-core:latest
WordPress Service Definition
Now, let’s integrate this into our docker-compose.yml. We’ll add the WordPress core service and a separate Nginx service that acts as a reverse proxy for the PHP-FPM container.
Update your docker-compose.yml:
[version: '3.7']
services:
# ... (db services from previous section) ...
wordpress:
image: your-dockerhub-username/wordpress-core:latest # Your custom image
restart: always
environment:
WORDPRESS_DB_HOST: wordpress_db_db-proxy:3306 # Use the ProxySQL service
WORDPRESS_DB_USER: wordpress_user
WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD}
WORDPRESS_DB_NAME: wordpress
volumes:
- wordpress_data:/var/www/html # Persistent storage for uploads, etc.
networks:
- app-network
depends_on:
- wordpress_db_db-proxy # Ensure DB proxy is available
deploy:
replicas: 3 # Scale WordPress instances
update_config:
parallelism: 2
delay: 10s
restart_policy:
condition: on-failure
wordpress-web:
image: nginx:latest
restart: always
ports:
- "80:80" # Public facing port
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro # Mount custom Nginx config
- wordpress_data:/var/www/html # Mount WordPress data for static assets
networks:
- app-network
depends_on:
- wordpress
deploy:
replicas: 3 # Scale Nginx instances
update_config:
parallelism: 2
delay: 10s
restart_policy:
condition: on-failure
volumes:
db_primary_data:
db_replica_data:
proxysql_data:
wordpress_data: # Persistent storage for WordPress files
networks:
app-network:
driver: overlay
Create a basic nginx.conf file for the wordpress-web service:
user www-data;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
ssl_protocols TLSv1.2 TLSv1.3; # Dropping older TLS versions
ssl_prefer_server_ciphers on;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
gzip on;
# Upstream for PHP-FPM
upstream php-fpm {
# Use Docker's service discovery to find the 'wordpress' service
# Swarm will load balance across replicas
server wordpress:9000;
}
server {
listen 80;
server_name localhost; # Replace with your domain
root /var/www/html;
index index.php index.html index.htm;
location / {
try_files $uri $uri/ /index.php?$args;
}
# Pass PHP scripts to FastCGI server
location ~ \.php$ {
include snippets/fastcgi-php.conf;
# Use Docker's service discovery to connect to the 'wordpress' service
fastcgi_pass wordpress:9000;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# Deny access to sensitive files
location ~ /\.ht {
deny all;
}
# Serve static files directly
location ~* \.(jpg|jpeg|gif|png|css|js|ico|webp|svg)$ {
expires 30d;
add_header Cache-Control "public";
}
}
}
Deploy this updated stack:
# Ensure environment variables are set export MYSQL_ROOT_PASSWORD='your_strong_root_password' export MYSQL_PASSWORD='your_strong_db_password' # Deploy the full stack docker stack deploy -c docker-compose.yml wordpress_stack
You can now access your WordPress admin panel and REST API via the IP address of any node running the wordpress-web service, on port 80.
Reverse Proxy and API Gateway (Traefik)
For production, you’ll want a dedicated reverse proxy like Traefik to handle SSL termination, dynamic routing, and potentially more advanced API gateway features. Traefik integrates seamlessly with Docker Swarm.
Traefik Configuration
Create a docker-compose.traefik.yml file:
[version: '3.7']
services:
traefik:
image: traefik:v2.9 # Use a specific version
command:
# Enable Docker provider
- "--providers.docker=true"
# Enable Docker Swarm provider
- "--providers.docker.swarmmode=true"
# Expose Traefik dashboard
- "--api.dashboard=true"
# Enable access logs
- "--accesslog=true"
# Set entrypoints (ports)
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
# Enable Let's Encrypt (replace with your domain and email)
- "--certificatesresolvers.myresolver.acme.tlschallenge=true"
- "--certificatesresolvers.myresolver.acme.email=your-email@example.com"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80" # HTTP
- "443:443" # HTTPS
- "8080:8080" # Traefik Dashboard
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt # For Let's Encrypt certificates
networks:
- traefik-public # A public network for external access
deploy:
replicas: 2
placement:
constraints:
- node.role == manager # Run Traefik on manager nodes
networks:
traefik-public:
external: true
You’ll need to create the traefik-public network if it doesn’t exist:
docker network create --driver overlay traefik-public
And create an empty letsencrypt/acme.json file and set its permissions:
mkdir letsencrypt touch letsencrypt/acme.json chmod 600 letsencrypt/acme.json
Configuring WordPress and Nginx for Traefik
We need to adjust our docker-compose.yml to use Traefik for routing and SSL. Remove the public port mapping from wordpress-web and add Traefik labels.
Update docker-compose.yml:
[version: '3.7']
services:
# ... (db services) ...
wordpress:
image: your-dockerhub-username/wordpress-core:latest
restart: always
environment:
WORDPRESS_DB_HOST: wordpress_db_db-proxy:3306
WORDPRESS_DB_USER: wordpress_user
WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD}
WORDPRESS_DB_NAME: wordpress
volumes:
- wordpress_data:/var/www/html
networks:
- app-network
- traefik-public # Connect to the public network
depends_on:
- wordpress_db_db-proxy
deploy:
replicas: 3
update_config:
parallelism: 2
delay: 10s
restart_policy:
condition: on-failure
labels:
# Traefik labels for routing to WordPress
- "traefik.enable=true"
- "traefik.http.routers.wordpress.rule=Host(`your-wordpress-domain.com`)" # Your domain
- "traefik.http.routers.wordpress.entrypoints=websecure" # Use HTTPS entrypoint
- "traefik.http.routers.wordpress.tls.certresolver=myresolver" # Use Let's Encrypt resolver
- "traefik.http.services.wordpress.loadbalancer.server.port=80" # Nginx listens on 80 internally
wordpress-web:
image: nginx:latest
restart: always
# REMOVE: ports: - "80:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- wordpress_data:/var/www/html
networks:
- app-network
- traefik-public # Connect to the public network
depends_on:
- wordpress
deploy:
replicas: 3
update_config:
parallelism: 2
delay: 10s
restart_policy:
condition: on-failure
labels:
# Traefik labels for routing to Nginx (which serves static assets and proxies PHP)
# This is technically redundant if wordpress router points to wordpress service,
# but good for clarity or if you had separate Nginx for static assets.
# For this setup, the wordpress router should point to the wordpress service,
# and the wordpress service's internal port is 80 (where nginx is listening).
# If you want Traefik to directly route to Nginx, you'd label wordpress-web instead.
# Let's assume wordpress router points to wordpress service, and wordpress service
# internally routes to nginx.
# If you want Traefik to route directly to Nginx:
- "traefik.enable=true"
- "traefik.http.routers.wordpress-web.rule=Host(`your-wordpress-domain.com`)"
- "traefik.http.routers.wordpress-web.entrypoints=websecure"
- "traefik.http.routers.wordpress-web.tls.certresolver=myresolver"
- "traefik.http.services.wordpress-web.loadbalancer.server.port=80" # Nginx listens on 80
volumes:
db_primary_data:
db_replica_data:
proxysql_data:
wordpress_data:
networks:
app-network:
driver: overlay
traefik-public:
external: true
Deploy Traefik first:
docker stack deploy -c docker-compose.traefik.yml traefik
Then redeploy your main WordPress stack:
# Ensure environment variables are set export MYSQL_ROOT_PASSWORD='your_strong_root_password' export MYSQL_PASSWORD='your_strong_db_password' # Deploy the full stack docker stack deploy -c docker-compose.yml wordpress_stack
Now, access your WordPress site via https://your-wordpress-domain.com. Traefik will handle SSL and route traffic to your WordPress instances. You can access the Traefik dashboard at http://.
Monitoring and Management
Docker Swarm provides basic monitoring and management capabilities:
- Service Status:
docker stack services [stack_name]anddocker service ps [service_name]. - Logs:
docker service logs [service_name]. - Resource Usage: Use Docker’s built-in stats or integrate with external monitoring tools like Prometheus/Grafana via exporters.
- Health Checks: Define health checks in your
docker-compose.yml(e.g., for Nginx, check/; for WordPress, check/wp-json/wp/v2/posts). Swarm will automatically restart unhealthy containers.
Advanced Considerations
Security:
- Use Docker Secrets for sensitive information (passwords, API keys) instead of environment variables in production.
- Configure firewall rules to restrict access to database ports.
- Regularly update Docker and your service images.
Scalability:
- Scale services up or down using
docker service scale [service_name]=N. - Monitor resource utilization and adjust replica counts accordingly.
- Consider dedicated nodes for database, application, and proxy services.
CI/CD:
- Automate Docker image builds and pushes to a registry.
- Use CI/CD pipelines to deploy updated stacks using
docker stack deploy.
Caching: Implement caching strategies (e.g., Redis, Varnish) for improved performance, especially for API responses.
Docker Swarm provides a powerful, yet often underestimated, platform for orchestrating complex applications like headless WordPress. By leveraging its native features and integrating with tools like Traefik, you can build a scalable, resilient, and secure architecture.