Architecting Resilient WordPress Headless Deployments with Docker, AWS ECS, and Advanced CI/CD Pipelines
Dockerizing WordPress and its Dependencies
The foundation of a resilient, scalable WordPress deployment lies in containerization. We’ll leverage Docker to package WordPress, its web server (Nginx), and its database (MySQL) into self-contained, reproducible units. This approach simplifies deployment, ensures consistency across environments, and facilitates horizontal scaling.
Our primary Dockerfile for WordPress will be a multi-stage build. The first stage will install necessary PHP extensions and dependencies, while the second stage will copy the WordPress core files and our custom theme/plugin code. We’ll use an official PHP-FPM image as the base for our application container.
WordPress Application Dockerfile
# Stage 1: Builder
FROM php:8.2-fpm-alpine AS builder
RUN apk add --no-cache \
git \
unzip \
libzip-dev \
libpng-dev \
libjpeg-turbo-dev \
freetype-dev \
icu-dev \
imagemagick-dev \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) gd \
&& docker-php-ext-install -j$(nproc) zip \
&& docker-php-ext-install -j$(nproc) intl \
&& pecl install imagick \
&& docker-php-ext-enable imagick \
&& apk del git unzip libzip-dev libpng-dev libjpeg-turbo-dev freetype-dev icu-dev imagemagick-dev
# Stage 2: Final Image
FROM php:8.2-fpm-alpine
COPY --from=builder /usr/local/lib/php/extensions/no-debug-non-zts-20220829/gd.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/
COPY --from=builder /usr/local/lib/php/extensions/no-debug-non-zts-20220829/zip.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/
COPY --from=builder /usr/local/lib/php/extensions/no-debug-non-zts-20220829/intl.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/
COPY --from=builder /usr/local/lib/php/extensions/no-debug-non-zts-20220829/imagick.so /usr/local/lib/php/extensions/no-debug-non-zts-20220829/
RUN docker-php-ext-enable gd zip intl imagick
# Install WordPress CLI for easier management
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 /usr/local/bin/wp
# Set up WordPress directory
WORKDIR /var/www/html
RUN chown -R www-data:www-data /var/www/html
# Copy custom theme and plugins (adjust paths as needed)
COPY ./wp-content/themes/your-theme /var/www/html/wp-content/themes/your-theme
COPY ./wp-content/plugins/your-plugin /var/www/html/wp-content/plugins/your-plugin
# Expose port 9000 for PHP-FPM
EXPOSE 9000
# Default command to run PHP-FPM
CMD ["php-fpm"]
For the web server, we’ll use an Nginx image. This container will be responsible for serving static assets and proxying dynamic requests to the PHP-FPM container. We’ll configure Nginx to handle SSL termination and serve content from a shared volume.
Nginx Configuration for WordPress
server {
listen 80;
server_name your-domain.com;
# Redirect HTTP to HTTPS
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
server_name your-domain.com;
# SSL Configuration
ssl_certificate /etc/nginx/ssl/your-domain.com.crt;
ssl_certificate_key /etc/nginx/ssl/your-domain.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_session_tickets off;
root /var/www/html;
index index.php index.html index.htm;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
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;
include fastcgi_params;
}
# Deny access to sensitive files
location ~ /\.ht {
deny all;
}
# Cache static assets
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|webp|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
}
}
The MySQL container will be a standard MySQL image. We’ll configure it with persistent storage using Docker volumes to ensure data durability. For production, consider using AWS RDS for managed database services.
Docker Compose for Local Development and Orchestration
A docker-compose.yml file orchestrates these containers, defining their networks, volumes, and dependencies. This is invaluable for local development and serves as a blueprint for our AWS ECS task definitions.
version: '3.8'
services:
db:
image: mysql:8.0
container_name: wordpress_db
volumes:
- db_data:/var/lib/mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpassword}
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-password}
networks:
- wordpress_network
wordpress:
build:
context: .
dockerfile: Dockerfile
container_name: wordpress_app
volumes:
- wp_content:/var/www/html/wp-content # Mount wp-content for persistent uploads and themes/plugins
ports:
- "8080:80" # Map host port 8080 to container port 80 for Nginx
depends_on:
- db
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD:-password}
WORDPRESS_DB_NAME: wordpress
networks:
- wordpress_network
restart: always
nginx:
image: nginx:latest
container_name: wordpress_nginx
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
- ./ssl:/etc/nginx/ssl:ro # Mount SSL certificates
- wp_content:/var/www/html/wp-content # Share wp-content with PHP-FPM for static assets
ports:
- "80:80"
- "443:443"
depends_on:
- wordpress
networks:
- wordpress_network
restart: always
volumes:
db_data:
wp_content:
networks:
wordpress_network:
To run this locally, ensure you have Docker and Docker Compose installed. Create a .env file in the same directory as docker-compose.yml for your database credentials:
MYSQL_ROOT_PASSWORD=your_root_password MYSQL_PASSWORD=your_db_password
Then, execute:
docker-compose up -d
Deploying to AWS Elastic Container Service (ECS)
AWS ECS provides a highly scalable, fast, and reliable way to deploy, manage, and scale containerized applications. We’ll define our WordPress application, Nginx, and database as separate ECS tasks, orchestrated by a cluster.
ECS Task Definitions
Task definitions are JSON files that describe how to run your container(s) on ECS. They specify the Docker image, CPU and memory requirements, ports, environment variables, and volumes.
WordPress Application Task Definition (Simplified JSON)
{
"family": "wordpress-app",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "1024",
"memory": "2048",
"executionRoleArn": "arn:aws:iam::YOUR_ACCOUNT_ID:role/ecsTaskExecutionRole",
"taskRoleArn": "arn:aws:iam::YOUR_ACCOUNT_ID:role/ecsTaskRole",
"containerDefinitions": [
{
"name": "wordpress",
"image": "YOUR_ECR_REPO_URI:latest",
"portMappings": [
{
"containerPort": 9000,
"protocol": "tcp"
}
],
"environment": [
{
"name": "WORDPRESS_DB_HOST",
"value": "your-rds-endpoint.region.rds.amazonaws.com:3306"
},
{
"name": "WORDPRESS_DB_USER",
"value": "wordpress"
},
{
"name": "WORDPRESS_DB_PASSWORD",
"valueFrom": "arn:aws:secretsmanager:YOUR_REGION:YOUR_ACCOUNT_ID:secret:your-rds-secret-XXXXXX:password::"
},
{
"name": "WORDPRESS_DB_NAME",
"value": "wordpress"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/wordpress-app",
"awslogs-region": "YOUR_REGION",
"awslogs-stream-prefix": "wordpress"
}
},
"mountPoints": [
{
"sourceVolume": "wp-content-volume",
"containerPath": "/var/www/html/wp-content"
}
]
}
],
"volumes": [
{
"name": "wp-content-volume",
"efsVolumeConfiguration": {
"fileSystemId": "fs-xxxxxxxxxxxxxxxxx",
"rootDirectoryPath": "/"
}
}
]
}
Note: For production, replace placeholders like YOUR_ACCOUNT_ID, YOUR_REGION, YOUR_ECR_REPO_URI, your-rds-endpoint, your-rds-secret-XXXXXX, and fs-xxxxxxxxxxxxxxxxx with your actual AWS resource identifiers. We’re using AWS Secrets Manager for database credentials and EFS for persistent storage of wp-content.
Nginx Task Definition (Simplified JSON)
{
"family": "wordpress-nginx",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "1024",
"memory": "2048",
"executionRoleArn": "arn:aws:iam::YOUR_ACCOUNT_ID:role/ecsTaskExecutionRole",
"taskRoleArn": "arn:aws:iam::YOUR_ACCOUNT_ID:role/ecsTaskRole",
"containerDefinitions": [
{
"name": "nginx",
"image": "nginx:latest",
"portMappings": [
{
"containerPort": 80,
"protocol": "tcp"
},
{
"containerPort": 443,
"protocol": "tcp"
}
],
"environment": [],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/wordpress-nginx",
"awslogs-region": "YOUR_REGION",
"awslogs-stream-prefix": "nginx"
}
},
"mountPoints": [
{
"sourceVolume": "wp-content-volume",
"containerPath": "/var/www/html/wp-content"
},
{
"sourceVolume": "nginx-config-volume",
"containerPath": "/etc/nginx/conf.d/default.conf",
"readOnly": true
},
{
"sourceVolume": "ssl-certs-volume",
"containerPath": "/etc/nginx/ssl",
"readOnly": true
}
]
}
],
"volumes": [
{
"name": "wp-content-volume",
"efsVolumeConfiguration": {
"fileSystemId": "fs-xxxxxxxxxxxxxxxxx",
"rootDirectoryPath": "/"
}
},
{
"name": "nginx-config-volume",
"host": {
"sourcePath": "/path/to/your/nginx.conf"
}
},
{
"name": "ssl-certs-volume",
"host": {
"sourcePath": "/path/to/your/ssl"
}
}
]
}
Important Considerations for Task Definitions:
- Network Mode:
awsvpcis recommended for Fargate, providing each task with its own Elastic Network Interface (ENI) and IP address. - Launch Type:
FARGATEabstracts away server management. EC2 launch type offers more control but requires managing EC2 instances. - IAM Roles:
ecsTaskExecutionRoleis required for pulling images and sending logs.ecsTaskRole(optional) grants permissions to the application itself (e.g., to access Secrets Manager). - Volumes: We use EFS for shared persistent storage of
wp-contentacross multiple tasks. For Nginx configuration and SSL certificates, we can use host-mounted volumes (if using EC2 launch type) or more commonly, store these in S3 and retrieve them during container startup, or use AWS Systems Manager Parameter Store. For simplicity in this example, host mounts are shown, but a production setup would likely use a more robust method for configuration. - Secrets Management: Never hardcode sensitive information like database passwords. Use AWS Secrets Manager or Parameter Store.
AWS Load Balancer and Service Configuration
An Application Load Balancer (ALB) distributes incoming traffic across your ECS tasks. We’ll configure listeners for HTTP (redirecting to HTTPS) and HTTPS, with SSL termination handled by the ALB itself or by the Nginx container.
An ECS Service manages the desired number of tasks and their deployment. It links the task definition, load balancer, and desired count.
Database Strategy: AWS RDS
For production WordPress deployments, using a managed database service like AWS RDS is highly recommended. It handles patching, backups, replication, and scaling, significantly reducing operational overhead. Ensure your ECS tasks are in the same VPC and subnets as your RDS instance, and configure RDS security groups to allow inbound traffic from your ECS tasks’ security group on port 3306.
Advanced CI/CD Pipelines with GitHub Actions
A robust CI/CD pipeline automates the build, test, and deployment process, ensuring rapid and reliable releases. We’ll use GitHub Actions to achieve this.
Pipeline Stages
- Build: Build Docker images for WordPress and Nginx. Tag them with the Git commit SHA and push to Amazon ECR.
- Test: Run unit tests, integration tests, and potentially end-to-end tests (e.g., using Cypress).
- Deploy to Staging: Update the ECS service to use the newly built Docker images for a staging environment.
- Manual Approval (Optional): A manual gate before deploying to production.
- Deploy to Production: Update the ECS service for the production environment.
GitHub Actions Workflow Example (.github/workflows/deploy.yml)
name: Deploy to AWS ECS
on:
push:
branches:
- main # Deploy main branch to production
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: YOUR_REGION
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build and push WordPress image
id: build-push-wp
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: wordpress-app # Your ECR repository name
IMAGE_TAG: ${{ github.sha }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
echo "::set-output name=tag::$IMAGE_TAG"
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
# Add similar steps for Nginx image if built separately
deploy-staging:
runs-on: ubuntu-latest
needs: build-and-push
if: github.ref == 'refs/heads/main' # Only run for main branch
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: YOUR_REGION
- name: Update ECS Service (Staging)
run: |
aws ecs update-service \
--cluster your-staging-cluster-name \
--service your-staging-service-name \
--task-definition your-staging-task-definition-family \
--force-new-deployment \
--no-cli-pager
# You might need to update task definition with new image URI first
# using aws ecs register-task-definition and then use its ARN here.
deploy-production:
runs-on: ubuntu-latest
needs: deploy-staging # Depends on successful staging deployment
if: github.ref == 'refs/heads/main'
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: YOUR_REGION
- name: Manual Approval (Optional)
uses: trstringer/manual-approval@v1
with:
secret: ${{ github.TOKEN }}
- name: Update ECS Service (Production)
run: |
aws ecs update-service \
--cluster your-production-cluster-name \
--service your-production-service-name \
--task-definition your-production-task-definition-family \
--force-new-deployment \
--no-cli-pager
# Similar logic for updating task definition with new image URI
Key elements in the workflow:
- AWS Credentials: Stored as GitHub secrets for secure access.
- ECR Integration: Uses
aws-actions/amazon-ecr-loginto authenticate and push images. - ECS Update: The
aws ecs update-servicecommand triggers a rolling deployment of new tasks with the updated image. For a robust deployment, you’d typically register a new task definition revision with the updated image URI before callingupdate-service. - Environment Separation: Separate clusters, services, and task definitions for staging and production environments.
- Manual Approval: A critical step for production deployments to prevent accidental rollouts.
Resilience and High Availability Strategies
To ensure your WordPress deployment remains available and performs well under load, consider these strategies:
Multi-AZ Deployments
Deploy your ECS cluster, ALB, and RDS instance across multiple Availability Zones (AZs) within a region. This protects against single points of failure at the data center level.
Auto Scaling
Configure ECS Service Auto Scaling to automatically adjust the number of running tasks based on metrics like CPU utilization, memory usage, or custom CloudWatch metrics (e.g., request count per target from the ALB). This ensures your application can handle traffic spikes and scales down to save costs during low traffic periods.
Database Read Replicas
For read-heavy WordPress sites, configure RDS Read Replicas. Direct read traffic to replicas to offload the primary database instance, improving overall performance and availability. Your application logic might need to be adapted to route queries appropriately.
Caching Strategies
Implement multiple layers of caching:
- Object Caching: Use Redis or Memcached (e.g., AWS ElastiCache) for WordPress object caching. Plugins like W3 Total Cache or WP Super Cache can be configured to use these.
- Page Caching: The Nginx configuration can serve static HTML versions of pages. Alternatively, use a CDN like CloudFront.
- CDN: Distribute static assets (images, CSS, JS) globally using a Content Delivery Network to reduce latency for end-users and offload traffic from your origin servers.
Health Checks
Configure both ALB target group health checks and ECS task health checks. The ALB health check should point to a simple PHP file (e.g., healthcheck.php) that returns a 200 OK status if WordPress is responsive. ECS health checks can monitor the container process itself.
Disaster Recovery and Backups
Regularly back up your WordPress files (wp-content) and your RDS database. AWS RDS provides automated backups and point-in-time restore capabilities. For file backups, consider AWS Backup or custom scripts that copy EFS data to S3.
By combining Docker for containerization, AWS ECS for orchestration, robust CI/CD pipelines, and strategic resilience patterns, you can architect a highly available, scalable, and maintainable headless WordPress deployment.