Dockerizing and Orchestrating Legacy WordPress Systems on Modern Google Cloud Infrastructure
Containerizing the WordPress Application
The first critical step in modernizing a legacy WordPress deployment is to containerize the application. This involves creating Docker images for the WordPress core, plugins, themes, and any custom PHP code. We’ll focus on a multi-stage build to keep our final image lean and secure.
We’ll start with a base PHP image that includes necessary extensions. For this example, we’ll use PHP 8.2 with FPM, as it’s a common and performant choice. The Dockerfile will handle installing WordPress, copying application code, and configuring PHP-FPM.
Dockerfile for WordPress Application
# Stage 1: Builder
FROM php:8.2-fpm-alpine AS builder
# Install necessary PHP extensions and system dependencies
RUN apk add --no-cache \
git \
unzip \
libzip-dev \
libpng-dev \
libjpeg-turbo-dev \
freetype-dev \
icu-dev \
imagemagick-dev \
libxml2-dev \
zip \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) gd \
&& docker-php-ext-install pdo pdo_mysql zip exif intl opcache
# Set working directory
WORKDIR /var/www/html
# Download and extract WordPress
RUN curl -o wordpress.tar.gz -SL https://wordpress.org/latest.tar.gz \
&& tar -xzf wordpress.tar.gz --strip-components=1 \
&& rm wordpress.tar.gz
# Copy custom themes, plugins, and uploads (if applicable)
# Ensure these directories exist in your build context
COPY ./wp-content/themes/ /var/www/html/wp-content/themes/
COPY ./wp-content/plugins/ /var/www/html/wp-content/plugins/
# COPY ./wp-content/uploads/ /var/www/html/wp-content/uploads/ # Be cautious with large uploads
# Copy custom PHP files or configurations
# COPY ./custom-php/ /var/www/html/
# Clean up build dependencies
RUN apk del git unzip libzip-dev libpng-dev libjpeg-turbo-dev freetype-dev icu-dev imagemagick-dev libxml2-dev \
&& rm -rf /var/cache/apk/*
# Stage 2: Production
FROM php:8.2-fpm-alpine AS production
# Install only runtime dependencies
RUN apk add --no-cache \
libzip \
libpng \
libjpeg-turbo \
freetype \
icu \
imagemagick \
libxml2 \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) gd \
&& docker-php-ext-install pdo pdo_mysql zip exif intl opcache
# Set working directory
WORKDIR /var/www/html
# Copy WordPress core and wp-content from the builder stage
COPY --from=builder /var/www/html/ /var/www/html/
# Ensure correct permissions for WordPress files
RUN chown -R www-data:www-data /var/www/html \
&& chmod -R 755 /var/www/html
# Expose the port PHP-FPM listens on
EXPOSE 9000
# Define the command to run PHP-FPM
CMD ["php-fpm"]
This Dockerfile uses a multi-stage build. The first stage (`builder`) installs build tools and extensions, downloads WordPress, and copies your custom content. The second stage (`production`) starts from a clean Alpine PHP image, installs only the necessary runtime libraries and PHP extensions, and then copies the built application from the `builder` stage. This results in a significantly smaller and more secure production image.
Nginx Configuration for WordPress
A robust web server like Nginx is essential for serving WordPress efficiently. We’ll create a separate Docker image for Nginx, configured to proxy requests to the PHP-FPM container and serve static assets directly.
Dockerfile for Nginx
FROM nginx:alpine # Remove default Nginx configuration RUN rm /etc/nginx/conf.d/default.conf # Copy custom Nginx configuration COPY nginx.conf /etc/nginx/conf.d/wordpress.conf # Copy custom favicon or error pages if needed # COPY html/ /usr/share/nginx/html/ # Expose port 80 EXPOSE 80 # Start Nginx in the foreground CMD ["nginx", "-g", "daemon off;"]
Nginx Configuration File (nginx.conf)
server {
listen 80;
server_name localhost; # Replace with your domain name
root /var/www/html;
index index.php index.html index.htm;
# Serve static files directly
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|webp|woff|woff2|ttf|eot)$ {
expires 30d;
add_header Cache-Control "public";
try_files $uri =404;
}
# Handle WordPress permalinks
location / {
try_files /index.php$is_args$args;
}
# Pass PHP scripts to FastCGI server
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass wordpress_app:9000; # Service name of the PHP-FPM container
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;
}
# Logging
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
}
In the nginx.conf, note the fastcgi_pass wordpress_app:9000; directive. This assumes your PHP-FPM container will be named wordpress_app when using Docker Compose. This configuration efficiently serves static assets and forwards PHP requests to the FPM process.
Database Containerization (MySQL/MariaDB)
A persistent and managed database is crucial. We’ll use the official MySQL or MariaDB Docker images. For production, it’s highly recommended to use a managed database service like Google Cloud SQL, but for demonstration and smaller deployments, a Dockerized database is viable.
Docker Compose for Local Orchestration
Docker Compose is an excellent tool for defining and running multi-container Docker applications. It allows us to orchestrate our WordPress app, Nginx, and database services.
docker-compose.yml
version: '3.8'
services:
wordpress_app:
build:
context: ./wordpress # Path to your WordPress Dockerfile directory
dockerfile: Dockerfile
container_name: wordpress_app
restart: always
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wordpress_user
WORDPRESS_DB_PASSWORD: wordpress_password
WORDPRESS_DB_NAME: wordpress_db
# Optional: For advanced configurations
# WP_HOME: http://localhost
# WP_SITEURL: http://localhost
volumes:
- wordpress_data:/var/www/html # Persist WordPress files
networks:
- app-network
nginx:
build:
context: ./nginx # Path to your Nginx Dockerfile directory
dockerfile: Dockerfile
container_name: nginx
restart: always
ports:
- "80:80" # Map host port 80 to container port 80
- "443:443" # If using SSL
volumes:
- wordpress_data:/var/www/html # Read-only access to WordPress files
- ./nginx/logs:/var/log/nginx # Persist Nginx logs
depends_on:
- wordpress_app
networks:
- app-network
db:
image: mysql:8.0 # Or mariadb:latest
container_name: wordpress_db
restart: always
environment:
MYSQL_ROOT_PASSWORD: root_password_here # CHANGE THIS
MYSQL_DATABASE: wordpress_db
MYSQL_USER: wordpress_user
MYSQL_PASSWORD: wordpress_password # CHANGE THIS
volumes:
- db_data:/var/lib/mysql # Persist database data
networks:
- app-network
volumes:
wordpress_data:
db_data:
networks:
app-network:
driver: bridge
This docker-compose.yml defines three services: wordpress_app (PHP-FPM), nginx, and db (MySQL). It sets up shared volumes for data persistence and a custom network for inter-container communication. Remember to replace placeholder passwords and adjust paths to your Dockerfile contexts.
Deploying to Google Kubernetes Engine (GKE)
For production environments, orchestrating these containers on a managed Kubernetes service like GKE is the standard. We’ll translate our Docker Compose setup into Kubernetes manifests.
Kubernetes Deployment for WordPress App
apiVersion: apps/v1
kind: Deployment
metadata:
name: wordpress-app-deployment
labels:
app: wordpress-app
spec:
replicas: 2 # Adjust as needed
selector:
matchLabels:
app: wordpress-app
template:
metadata:
labels:
app: wordpress-app
spec:
containers:
- name: wordpress-app
image: your-gcr-repo/wordpress-app:latest # Replace with your GCR image
ports:
- containerPort: 9000
env:
- name: WORDPRESS_DB_HOST
value: "mysql-service" # Kubernetes Service name for MySQL
- name: WORDPRESS_DB_USER
value: "wordpress_user"
- name: WORDPRESS_DB_PASSWORD
valueFrom:
secretKeyRef:
name: wordpress-db-secrets
key: db-password
- name: WORDPRESS_DB_NAME
value: "wordpress_db"
volumeMounts:
- name: wordpress-persistent-storage
mountPath: /var/www/html
volumes:
- name: wordpress-persistent-storage
persistentVolumeClaim:
claimName: wordpress-pvc
Kubernetes Deployment for Nginx
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 2 # Adjust as needed
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: your-gcr-repo/nginx:latest # Replace with your GCR image
ports:
- containerPort: 80
- containerPort: 443 # If using SSL
volumeMounts:
- name: nginx-logs
mountPath: /var/log/nginx
- name: wordpress-persistent-storage # Mount WordPress files read-only
mountPath: /var/www/html
readOnly: true
volumes:
- name: nginx-logs
emptyDir: {} # Or use a PersistentVolumeClaim for logs
- name: wordpress-persistent-storage
persistentVolumeClaim:
claimName: wordpress-pvc
Kubernetes Deployment for MySQL/MariaDB
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql-deployment
labels:
app: mysql
spec:
replicas: 1 # Typically one replica for a database
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0 # Or mariadb:latest
ports:
- containerPort: 3306
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: wordpress-db-secrets
key: root-password
- name: MYSQL_DATABASE
value: "wordpress_db"
- name: MYSQL_USER
value: "wordpress_user"
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: wordpress-db-secrets
key: db-password
volumeMounts:
- name: mysql-persistent-storage
mountPath: /var/lib/mysql
volumes:
- name: mysql-persistent-storage
persistentVolumeClaim:
claimName: mysql-pvc
Kubernetes Services and Persistent Volumes
apiVersion: v1
kind: Service
metadata:
name: wordpress-app-service
spec:
selector:
app: wordpress-app
ports:
- protocol: TCP
port: 9000
targetPort: 9000
type: ClusterIP # Internal service
---
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
- protocol: TCP
port: 443
targetPort: 443
type: LoadBalancer # Expose Nginx to the internet
---
apiVersion: v1
kind: Service
metadata:
name: mysql-service
spec:
selector:
app: mysql
ports:
- protocol: TCP
port: 3306
targetPort: 3306
type: ClusterIP # Internal service
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: wordpress-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi # Adjust size as needed
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi # Adjust size as needed
Kubernetes Secrets for Database Credentials
apiVersion: v1 kind: Secret metadata: name: wordpress-db-secrets type: Opaque data: db-password: YOUR_DB_PASSWORD_BASE64_ENCODED # e.g., echo -n 'your_password' | base64 root-password: YOUR_ROOT_PASSWORD_BASE64_ENCODED # e.g., echo -n 'your_root_password' | base64
Before applying these manifests, ensure you have built your Docker images and pushed them to a container registry accessible by GKE (e.g., Google Container Registry – GCR). You’ll also need to create the Kubernetes secrets with your database credentials, base64 encoded. The nginx-service is of type LoadBalancer, which will provision a Google Cloud Load Balancer to expose your WordPress site. For production, consider using Google Cloud SQL for a managed database solution, which simplifies operations and improves reliability.
Advanced Considerations: Caching, CDN, and Security
To truly leverage modern infrastructure, consider these enhancements:
- Object Caching: Integrate Redis or Memcached for WordPress object caching. This requires adding a Redis/Memcached service to your Kubernetes manifests and configuring WordPress (e.g., via a plugin like W3 Total Cache or Redis Object Cache) to use it.
- CDN Integration: Use Google Cloud CDN or a third-party CDN to serve static assets. This offloads traffic from your Nginx pods and improves global performance.
- SSL/TLS Termination: For production, terminate SSL at the Google Cloud Load Balancer (managed by the
LoadBalancerservice type) or use an Ingress controller with cert-manager for automated certificate management. - Database Management: As mentioned, Google Cloud SQL is the recommended path for production databases. It offers automated backups, replication, and high availability.
- Monitoring and Logging: Integrate Google Cloud’s operations suite (formerly Stackdriver) for comprehensive monitoring, logging, and alerting.
By containerizing your legacy WordPress system and orchestrating it on GKE, you gain scalability, resilience, and the ability to manage your application using modern DevOps practices. This transition is a significant step towards a more robust and maintainable infrastructure.