Building a High-Availability, Cost-Optimized WordPress Stack on DigitalOcean
Architectural Overview: HA WordPress on DigitalOcean
This post details the construction of a highly available and cost-optimized WordPress stack on DigitalOcean, targeting CTOs and VPs of Engineering. We will leverage managed services and strategic instance sizing to balance performance, resilience, and operational expenditure. The core components include DigitalOcean Kubernetes (DOKS) for application orchestration, managed PostgreSQL for database reliability, and a robust caching layer.
Database Layer: Managed PostgreSQL for Resilience
A single-point-of-failure database is unacceptable for a high-availability setup. DigitalOcean’s Managed PostgreSQL offers automated backups, point-in-time recovery, and read replicas, significantly reducing operational overhead and improving resilience. We’ll provision a cluster suitable for moderate to high traffic, considering IOPS and memory requirements.
Provisioning Steps:
- Navigate to the DigitalOcean control panel.
- Select “Databases” from the left-hand menu.
- Click “Create PostgreSQL Cluster”.
- Choose a region geographically close to your DOKS cluster.
- Select a “Production” plan. For initial deployment, a “Basic” plan with 2 vCPU, 4GB RAM, and 40GB storage is a reasonable starting point. This can be scaled later.
- Configure automated backups (default is 7 days, adjust as needed).
- Enable “Point-in-time recovery”.
- Note the connection details (host, port, username, password, database name) for later use.
Kubernetes Cluster Setup: DigitalOcean Kubernetes (DOKS)
DOKS provides a managed Kubernetes control plane, simplifying cluster operations. We’ll deploy WordPress and its dependencies as containerized applications within DOKS.
DOKS Cluster Configuration:
- Create a new DOKS cluster.
- Region: Match your PostgreSQL cluster’s region.
- Kubernetes Version: Select the latest stable version.
- Node Pool: For cost optimization and HA, we’ll use multiple node pools.
- Node Pool 1 (WordPress Workers): Use general-purpose nodes (e.g., `s-2vcpu-4gb`). Configure 3 nodes for basic HA. This pool will run the WordPress pods.
- Node Pool 2 (Cache/Background Workers): Use memory-optimized nodes (e.g., `m-2vcpu-8gb`) if running Redis or other memory-intensive tasks. This can be scaled down initially.
- VPC Network: Enable VPC for secure internal communication.
Containerizing WordPress and Dependencies
We’ll use Docker to containerize WordPress, PHP-FPM, and Nginx. For cost optimization, we’ll use lean base images and multi-stage builds.
Dockerfile for WordPress (PHP-FPM & Nginx):
This example uses a multi-stage build to keep the final image small. We’ll use `php:8.2-fpm-alpine` as the base.
# Stage 1: Build PHP application
FROM php:8.2-fpm-alpine AS php_builder
RUN apk add --no-cache \
git \
zip \
unzip \
libzip-dev \
libpng-dev \
libjpeg-turbo-dev \
freetype-dev \
icu-dev \
postgresql-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) pdo pdo_pgsql \
&& docker-php-ext-install -j$(nproc) intl \
&& apk del libzip-dev libpng-dev libjpeg-turbo-dev freetype-dev icu-dev postgresql-dev
WORKDIR /var/www/html
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
RUN composer global require "hirak/prestissimo" --no-interaction \
&& composer config -g repos.packagist composer/download.php:https://packagist.org/mirror.json
# Copy WordPress core and plugins/themes (assuming they are in a local 'src' directory)
COPY src/ /var/www/html/
# Install Composer dependencies
RUN composer install --no-dev --optimize-autoloader --no-interaction
# Stage 2: Production Nginx + PHP-FPM
FROM nginx:alpine
COPY --from=php_builder /var/www/html /var/www/html
COPY --from=php_builder /usr/local/etc/php/conf.d/docker-php-ext-gd.ini /usr/local/etc/php/conf.d/docker-php-ext-gd.ini
COPY --from=php_builder /usr/local/etc/php/conf.d/docker-php-ext-zip.ini /usr/local/etc/php/conf.d/docker-php-ext-zip.ini
COPY --from=php_builder /usr/local/etc/php/conf.d/docker-php-ext-pdo.ini /usr/local/etc/php/conf.d/docker-php-ext-pdo.ini
COPY --from=php_builder /usr/local/etc/php/conf.d/docker-php-ext-pdo_pgsql.ini /usr/local/etc/php/conf.d/docker-php-ext-pdo_pgsql.ini
COPY --from=php_builder /usr/local/etc/php/conf.d/docker-php-ext-intl.ini /usr/local/etc/php/conf.d/docker-php-ext-intl.ini
# Copy custom Nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Ensure correct permissions
RUN chown -R www-data:www-data /var/www/html \
&& chmod -R 755 /var/www/html
# Expose port 80
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
nginx.conf:
server {
listen 80;
server_name localhost;
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_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass php-fpm:9000; # Assumes a separate PHP-FPM service or sidecar
fastcgi_index index.php;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
include fastcgi_params;
}
# Deny access to sensitive files
location ~ /\.ht {
deny all;
}
# Caching headers for static assets
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
}
}
Note: The `fastcgi_pass php-fpm:9000;` directive assumes PHP-FPM is running on a separate container/service named `php-fpm`. In a DOKS deployment, this would typically be a separate Deployment and Service for PHP-FPM, or a sidecar container within the WordPress pod.
Kubernetes Manifests: Deploying WordPress
We’ll define Kubernetes resources using YAML manifests. This includes Deployments for WordPress and PHP-FPM, Services for internal and external access, and Ingress for routing.
1. Namespace:
apiVersion: v1 kind: Namespace metadata: name: wordpress
2. WordPress Deployment:
This deployment will run our Nginx/PHP-FPM container. We’ll use a Horizontal Pod Autoscaler (HPA) for automatic scaling based on CPU utilization.
apiVersion: apps/v1
kind: Deployment
metadata:
name: wordpress
namespace: wordpress
labels:
app: wordpress
spec:
replicas: 2 # Start with 2 replicas for HA
selector:
matchLabels:
app: wordpress
template:
metadata:
labels:
app: wordpress
spec:
containers:
- name: wordpress
image: your-dockerhub-username/wordpress-ha:latest # Replace with your image
ports:
- containerPort: 80
env:
- name: DB_HOST
value: "your-do-managed-pg-host.ondigitalocean.com" # From DO Managed PostgreSQL
- name: DB_USER
valueFrom:
secretKeyRef:
name: wordpress-db-secrets
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: wordpress-db-secrets
key: password
- name: DB_NAME
value: "wordpress" # Or your specific database name
- name: WP_SITEURL
value: "https://yourdomain.com" # Your primary domain
- name: WP_HOME
value: "https://yourdomain.com" # Your primary domain
resources:
requests:
cpu: "200m"
memory: "512Mi"
limits:
cpu: "500m"
memory: "1Gi"
livenessProbe:
httpGet:
path: /wp-admin/admin-ajax.php?action=heartbeat # A simple check
port: 80
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
volumeMounts:
- name: wp-content
mountPath: /var/www/html/wp-content
volumes:
- name: wp-content
emptyDir: {} # Will be replaced by a persistent volume or shared storage
3. WordPress Database Secrets:
apiVersion: v1 kind: Secret metadata: name: wordpress-db-secrets namespace: wordpress type: Opaque data: username:password:
4. WordPress Service (Internal):
apiVersion: v1
kind: Service
metadata:
name: wordpress-internal
namespace: wordpress
spec:
selector:
app: wordpress
ports:
- protocol: TCP
port: 80
targetPort: 80
type: ClusterIP
5. Horizontal Pod Autoscaler (HPA):
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
name: wordpress-hpa
namespace: wordpress
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: wordpress
minReplicas: 2
maxReplicas: 10 # Adjust based on expected load and cost
targetCPUUtilizationPercentage: 70
Persistent Storage for `wp-content`
The `emptyDir` volume in the deployment is ephemeral. For `wp-content` (uploads, themes, plugins), we need persistent storage. DigitalOcean Block Storage can be provisioned as Persistent Volumes (PVs) and Persistent Volume Claims (PVCs) in DOKS. For better performance and HA, consider using a shared filesystem solution like NFS or a managed object storage service with a WordPress plugin.
Option A: DigitalOcean Block Storage (Simpler, but not truly shared):
# Example PersistentVolumeClaim (PVC)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: wordpress-content-pvc
namespace: wordpress
spec:
accessModes:
- ReadWriteOnce # Important: Block storage is typically RWO
resources:
requests:
storage: 50Gi # Adjust size as needed
storageClassName: do-block-storage # Or your specific DO CSI driver class
If using RWO Block Storage, you’ll need to ensure only one pod mounts it at a time, or use a mechanism to synchronize writes. This is often problematic for WordPress uploads. A better approach for HA is a shared filesystem.
Option B: NFS (More Complex, but Shared):
Deploy an NFS server (e.g., on a separate Droplet or using a managed NFS service) and configure DOKS to use it. This involves setting up an NFS provisioner or manually creating PVs pointing to your NFS share.
Option C: Object Storage (e.g., S3-compatible):
Use a WordPress plugin (like WP Offload Media Lite) to store uploads directly to DigitalOcean Spaces or another S3-compatible object storage. This offloads storage from your Kubernetes cluster and is highly scalable and cost-effective.
Caching Layer: Redis for Performance
Implementing Redis significantly improves WordPress performance by caching database queries and object data. We’ll deploy Redis as a separate Deployment and Service within DOKS.
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: wordpress
labels:
app: redis
spec:
replicas: 1 # Redis master, consider Sentinel for HA Redis if needed
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- name: redis
image: redis:alpine
ports:
- containerPort: 6379
resources:
requests:
cpu: "50m"
memory: "128Mi"
limits:
cpu: "100m"
memory: "256Mi"
---
apiVersion: v1
kind: Service
metadata:
name: redis-service
namespace: wordpress
spec:
selector:
app: redis
ports:
- protocol: TCP
port: 6379
targetPort: 6379
type: ClusterIP
You’ll need a WordPress plugin (like W3 Total Cache or Redis Object Cache) configured to use this Redis instance. Update your `wp-config.php` or plugin settings with `redis-service:6379` as the host.
Ingress Controller and External Access
An Ingress controller manages external access to services within the cluster. DigitalOcean provides a managed Kubernetes Load Balancer that can be integrated with an Ingress controller.
1. Install Nginx Ingress Controller:
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx helm repo update helm install ingress-nginx ingress-nginx/ingress-nginx \ --namespace ingress-nginx --create-namespace \ --set controller.replicaCount=2 \ --set controller.nodeSelector."kubernetes\.io/os"=linux \ --set controller.admissionWebhooks.patch.nodeSelector."kubernetes\.io/os"=linux \ --set controller.service.type=LoadBalancer \ --set controller.service.loadBalancerIP="YOUR_STATIC_IP_ADDRESS" # Optional: Use a reserved static IP
Note: Replace `YOUR_STATIC_IP_ADDRESS` with a reserved static IP from DigitalOcean if you want a stable external IP. Otherwise, the LoadBalancer service will provision a dynamic IP.
2. WordPress Ingress Resource:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: wordpress-ingress
namespace: wordpress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
# Add other Nginx specific annotations as needed (e.g., SSL, caching)
spec:
ingressClassName: nginx # Ensure this matches your Ingress Controller's class
rules:
- host: yourdomain.com # Replace with your domain
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: wordpress-internal
port:
number: 80
tls: # Optional: For HTTPS
- hosts:
- yourdomain.com
secretName: yourdomain-tls-secret # Kubernetes secret containing your TLS certificate
Cost Optimization Strategies
Instance Sizing: Start with smaller, cost-effective Droplets for your node pools and scale up or add more nodes as needed. Monitor resource utilization closely.
Managed Services: While Managed PostgreSQL has a cost, it’s often cheaper than self-managing a highly available database cluster with backups and failover on Kubernetes. Evaluate the trade-offs.
Auto-Scaling: Configure HPA for WordPress pods and adjust `maxReplicas` to prevent over-provisioning. Scale down non-critical node pools during off-peak hours if possible (requires more advanced automation).
Resource Requests/Limits: Set appropriate CPU and memory requests and limits for your containers. This ensures efficient resource allocation and prevents noisy neighbor issues.
Object Storage for Media: Offloading media uploads to DigitalOcean Spaces is significantly cheaper than persistent block storage for large volumes of data.
Reserved IPs: For the LoadBalancer, using a reserved static IP incurs a small monthly cost but guarantees stability. Evaluate if this is necessary over a dynamic IP.
Monitoring and Maintenance
Implement robust monitoring using Prometheus and Grafana (often available as add-ons in DOKS or deployable via Helm). Key metrics to track include:
- Pod CPU/Memory utilization (for HPA tuning)
- Node resource usage
- Database connection counts and query latency
- Redis hit/miss ratio
- Ingress controller request rates and error codes (5xx, 4xx)
- Application-level metrics (e.g., WordPress heartbeat, page load times)
Regularly review DigitalOcean billing and resource utilization to identify further optimization opportunities. Keep Kubernetes versions and node images up-to-date for security and performance.