Dockerizing and Orchestrating Legacy WordPress Systems on Modern Linode Infrastructure
Assessing Legacy WordPress for Containerization
Before embarking on the containerization journey for a legacy WordPress installation, a thorough assessment is paramount. This involves identifying dependencies, understanding the existing infrastructure, and evaluating the potential benefits and challenges. Legacy systems often have tightly coupled components, custom PHP versions, or specific server configurations that need careful consideration. We’ll focus on a common scenario: a WordPress site with a MySQL database and potentially some custom PHP plugins or themes that might rely on specific PHP extensions or configurations.
Crafting the Dockerfile for WordPress and MySQL
We’ll start by defining our Docker environment. A multi-stage build is beneficial for optimizing the final image size and security. For this example, we’ll use official WordPress and MySQL images as our base, layering our customizations on top.
First, let’s define the WordPress Dockerfile. This will handle the WordPress core, themes, and plugins. We’ll assume a need for specific PHP extensions and a custom `wp-config.php`.
# Stage 1: Builder
FROM php:8.2-fpm-alpine AS builder
# Install necessary PHP extensions for WordPress and common plugins
RUN apk update && apk add --no-cache \
libzip-dev \
libpng-dev \
libjpeg-turbo-dev \
freetype-dev \
icu-dev \
imagemagick-dev \
git \
zip \
unzip \
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
&& docker-php-ext-install -j$(nproc) gd zip exif \
&& pecl install imagick \
&& docker-php-ext-enable imagick \
&& apk del libzip-dev libpng-dev libjpeg-turbo-dev freetype-dev icu-dev imagemagick-dev git
# Stage 2: Production
FROM php:8.2-fpm-alpine
# Copy installed extensions from builder stage
COPY --from=builder /usr/local/lib/php/extensions/no-debug-non-zts-20220829/ /usr/local/lib/php/extensions/no-debug-non-zts-20220829/
# Install runtime dependencies
RUN apk update && apk add --no-cache \
libzip \
libpng \
libjpeg-turbo \
freetype \
icu \
imagemagick \
ghostscript \
tzdata \
&& docker-php-ext-enable gd imagick zip exif
# Set timezone
ENV TZ=UTC
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer
# Set working directory
WORKDIR /var/www/html
# Download WordPress core
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 wp-config.php (if any)
# COPY wp-config.php /var/www/html/wp-config.php
# Copy custom themes and plugins (if needed)
# COPY wp-content/themes/ /var/www/html/wp-content/themes/
# COPY wp-content/plugins/ /var/www/html/wp-content/plugins/
# Ensure correct permissions
RUN chown -R www-data:www-data /var/www/html && \
chmod -R 755 /var/www/html
# Expose port
EXPOSE 9000
# Default command to run PHP-FPM
CMD ["php-fpm"]
Next, the `docker-compose.yml` file orchestrates the WordPress and MySQL services. This configuration defines the services, their images, ports, volumes, and network settings. We’ll use persistent volumes for MySQL data and WordPress uploads to ensure data durability.
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}
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress_user
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
networks:
- wordpress_network
wordpress:
build:
context: .
dockerfile: Dockerfile
container_name: wordpress_app
ports:
- "80:80"
volumes:
- wp_content:/var/www/html/wp-content
- wp_uploads:/var/www/html/wp-content/uploads
restart: always
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_NAME: wordpress
WORDPRESS_DB_USER: wordpress_user
WORDPRESS_DB_PASSWORD: ${MYSQL_PASSWORD}
depends_on:
- db
networks:
- wordpress_network
volumes:
db_data:
wp_content:
wp_uploads:
networks:
wordpress_network:
driver: bridge
To manage environment variables like database passwords, create a `.env` file in the same directory as your `docker-compose.yml`:
MYSQL_ROOT_PASSWORD=your_strong_root_password MYSQL_PASSWORD=your_strong_wordpress_password
Deploying to Linode Kubernetes Engine (LKE)
Linode Kubernetes Engine (LKE) provides a managed Kubernetes experience, simplifying cluster management. We’ll translate our `docker-compose` setup into Kubernetes manifests.
First, ensure you have `kubectl` configured to communicate with your LKE cluster. You can download your cluster’s kubeconfig file from the Linode Cloud Manager.
Kubernetes Deployment for MySQL
This deployment defines the MySQL stateful application. Using a `StatefulSet` is crucial for databases as it provides stable network identifiers and persistent storage.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: wordpress-db
labels:
app: wordpress-db
spec:
serviceName: wordpress-db
replicas: 1
selector:
matchLabels:
app: wordpress-db
template:
metadata:
labels:
app: wordpress-db
spec:
containers:
- name: mysql
image: mysql:8.0
ports:
- containerPort: 3306
name: mysql
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secrets
key: root-password
- name: MYSQL_DATABASE
value: "wordpress"
- name: MYSQL_USER
value: "wordpress_user"
- name: MYSQL_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secrets
key: password
volumeMounts:
- name: mysql-persistent-storage
mountPath: /var/lib/mysql
volumeClaimTemplates:
- metadata:
name: mysql-persistent-storage
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 10Gi # Adjust storage size as needed
storageClassName: linode-block-storage # Or your preferred Linode storage class
Kubernetes Deployment for WordPress
The WordPress deployment uses a `Deployment` for the application pods and a `Service` to expose it. We’ll use `PersistentVolumeClaims` for `wp-content` and `wp-uploads` to ensure data persistence.
apiVersion: apps/v1
kind: Deployment
metadata:
name: wordpress-app
labels:
app: wordpress-app
spec:
replicas: 2 # Scale WordPress pods for high availability
selector:
matchLabels:
app: wordpress-app
template:
metadata:
labels:
app: wordpress-app
spec:
containers:
- name: wordpress
image: your-dockerhub-username/wordpress-legacy:latest # Replace with your image
ports:
- containerPort: 80
env:
- name: WORDPRESS_DB_HOST
value: "wordpress-db-0.wordpress-db.default.svc.cluster.local:3306" # Service name for MySQL
- name: WORDPRESS_DB_NAME
value: "wordpress"
- name: WORDPRESS_DB_USER
value: "wordpress_user"
- name: WORDPRESS_DB_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secrets
key: password
volumeMounts:
- name: wp-content-volume
mountPath: /var/www/html/wp-content
- name: wp-uploads-volume
mountPath: /var/www/html/wp-content/uploads
volumes:
- name: wp-content-volume
persistentVolumeClaim:
claimName: wp-content-pvc
- name: wp-uploads-volume
persistentVolumeClaim:
claimName: wp-uploads-pvc
---
apiVersion: v1
kind: Service
metadata:
name: wordpress-app-service
spec:
selector:
app: wordpress-app
ports:
- protocol: TCP
port: 80
targetPort: 80
type: LoadBalancer # Use LoadBalancer for external access
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: wp-content-pvc
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 5Gi # Adjust storage size as needed
storageClassName: linode-block-storage # Or your preferred Linode storage class
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: wp-uploads-pvc
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 10Gi # Adjust storage size as needed
storageClassName: linode-block-storage # Or your preferred Linode storage class
Kubernetes Secrets for Credentials
It’s critical to manage sensitive information like database passwords securely. Kubernetes Secrets are the standard way to do this.
apiVersion: v1
kind: Secret
metadata:
name: mysql-secrets
type: Opaque
data:
root-password: ${MYSQL_ROOT_PASSWORD} # Base64 encoded
password: ${MYSQL_PASSWORD} # Base64 encoded
Note: You’ll need to base64 encode your passwords before putting them into the `data` section of the Secret manifest. For example, on Linux/macOS:
echo -n 'your_strong_root_password' | base64 echo -n 'your_strong_wordpress_password' | base64
Nginx Ingress Controller for Advanced Routing
For production environments, an Ingress controller is essential for managing external access to services, SSL termination, and advanced routing rules. We’ll deploy the Nginx Ingress Controller and configure an Ingress resource for our WordPress application.
Deploying Nginx Ingress Controller
You can install the Nginx Ingress Controller using Helm or by applying its YAML manifests directly. Using Helm is generally recommended for easier management and upgrades.
# Add the ingress-nginx Helm repository helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx helm repo update # Install the ingress-nginx controller 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 defaultBackend.nodeSelector."kubernetes\.io/os"=linux
After installation, obtain the external IP address of the Ingress controller. This will be the public IP for your WordPress site.
kubectl get svc -n ingress-nginx
Configuring the WordPress Ingress Resource
This Ingress resource directs external traffic to the `wordpress-app-service`.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: wordpress-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
# Add other Nginx specific annotations as needed, e.g., for SSL
spec:
ingressClassName: nginx # Ensure this matches your Ingress Controller's class
rules:
- host: your-domain.com # Replace with your actual domain
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: wordpress-app-service
port:
number: 80
Migration and Post-Deployment Considerations
Migrating a live legacy WordPress site requires careful planning to minimize downtime. This typically involves a database dump and restore, followed by file synchronization.
Database Migration Strategy
1. Downtime Window: Announce a maintenance window for the migration.
2. Database Dump: On the legacy server, create a dump of the WordPress database.
mysqldump -u [legacy_db_user] -p [legacy_db_name] > wordpress_backup.sql
3. Transfer Dump: Securely transfer `wordpress_backup.sql` to a location accessible by your LKE cluster (e.g., a temporary pod or your local machine).
4. Import into LKE MySQL: Use `kubectl exec` to run the import command within the MySQL pod.
# First, ensure your mysql pod is running and get its name
kubectl get pods -l app=wordpress-db
# Then, execute the import command
kubectl exec -it wordpress-db-0 -- mysql -u root -p"${MYSQL_ROOT_PASSWORD}" wordpress < wordpress_backup.sql
File Synchronization
Synchronize the `wp-content` directory (especially `uploads`) from the legacy server to the persistent volumes in LKE. This can be done using `rsync`.
# On your local machine or a bastion host rsync -avz --progress /path/to/legacy/wp-content/uploads/ user@your-linode-ip:/path/to/your/wp-uploads/volume/
You’ll need to identify the exact mount path for your `wp-uploads` volume within the WordPress pod. You can find this by inspecting the pod definition or by exec-ing into the pod and checking.
DNS Update
Once the data is migrated and verified, update your domain’s DNS records to point to the external IP address of your Nginx Ingress Controller service.
Monitoring and Maintenance
Post-deployment, robust monitoring is crucial. Utilize Linode’s monitoring tools, Prometheus, and Grafana for cluster and application-level metrics. Regularly check logs using `kubectl logs` and consider a centralized logging solution like ELK stack or Loki.
Regularly update your Docker images, Kubernetes manifests, and Helm charts to incorporate security patches and new features. Automate these processes using CI/CD pipelines for seamless deployments.