Running WordPress on Kubernetes might seem like using a sledgehammer to crack a nut, but when you need scalability, high availability, and the ability to handle traffic spikes during your viral blog post moments, Kubernetes becomes your best friend. In this comprehensive guide, we’ll walk you through everything you need to know about deploying WordPress on Kubernetes in a production-ready configuration. No shortcuts, no “works on my machine” solutions—just battle-tested practices that you can deploy with confidence.
Understanding WordPress on Kubernetes Architecture Before diving into commands, let’s understand how all the pieces fit together:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 ┌─────────────────────────────────────────────────────────────┐ │ Load Balancer │ │ (Ingress Controller) │ └──────────────────────┬──────────────────────────────────────┘ │ ┌──────────────┴──────────────┐ │ │ ┌───────▼────────┐ ┌────────▼────────┐ │ WordPress Pod 1│ │ WordPress Pod 2 │ │ │ │ │ │ - PHP-FPM │ │ - PHP-FPM │ │ - Nginx/Apache │ │ - Nginx/Apache │ └────────┬───────┘ └────────┬────────┘ │ │ └──────────┬─────────────────┘ │ ┌──────────▼─────────┐ │ MySQL Service │ └──────────┬─────────┘ │ ┌───────────────┼───────────────┐ │ │ │ ┌───▼────┐ ┌───▼────┐ ┌───▼────┐ │MySQL-0 │ │MySQL-1 │ │MySQL-2 │ │Primary │───▶│Replica │───▶│Replica │ └───┬────┘ └───┬────┘ └───┬────┘ │ │ │ ┌───▼────┐ ┌───▼────┐ ┌───▼────┐ │ PVC │ │ PVC │ │ PVC │ │ (50Gi) │ │ (50Gi) │ │ (50Gi) │ └────────┘ └────────┘ └────────┘ ┌──────────────────┐ │ Shared Storage │ │ (WordPress Files)│ │ - Themes │ │ - Plugins │ │ - Uploads │ └──────────────────┘
Prerequisites and Environment Setup
A running Kubernetes cluster (v1.24+)
kubectl configured to interact with your cluster
Helm installed (v3.8+)
A Storage Class for persistent storage
An Ingress Controller (nginx or traefik)
Resource Requirements 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Total CPU: 4 cores Total Memory: 8 GB RAM Storage: 100 GB persistent storage Per WordPress Pod: Requests: CPU: 250m Memory: 512Mi Limits: CPU: 500m Memory: 1Gi Per MySQL Pod: Requests: CPU: 500m Memory: 1Gi Limits: CPU: 1000m Memory: 2Gi
Quick Environment Verification Run these commands to verify your setup:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 kubectl cluster-info kubectl version --client kubectl get storageclass kubectl get pods -n ingress-nginx helm version
Setting Up MySQL Database with StatefulSet WordPress needs a reliable database. Let’s deploy MySQL with high availability using StatefulSets.
Step 1: Create Namespace and Secrets 1 2 3 4 5 6 7 8 9 10 kubectl create namespace wordpress kubectl create secret generic mysql-pass \ --from-literal=password='YourSecurePassword123!' \ -n wordpress kubectl get secrets -n wordpress
Step 2: Deploy MySQL with Persistent Storage Create a file named mysql-deployment.yaml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 apiVersion: v1 kind: Service metadata: name: wordpress-mysql namespace: wordpress labels: app: wordpress spec: ports: - port: 3306 targetPort: 3306 selector: app: wordpress tier: mysql clusterIP: None --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mysql-pvc namespace: wordpress labels: app: wordpress spec: accessModes: - ReadWriteOnce resources: requests: storage: 50Gi storageClassName: standard --- apiVersion: apps/v1 kind: Deployment metadata: name: wordpress-mysql namespace: wordpress labels: app: wordpress spec: selector: matchLabels: app: wordpress tier: mysql strategy: type: Recreate template: metadata: labels: app: wordpress tier: mysql spec: containers: - name: mysql image: mysql:8.0 env: - name: MYSQL_ROOT_PASSWORD valueFrom: secretKeyRef: name: mysql-pass key: password - name: MYSQL_DATABASE value: wordpress - name: MYSQL_USER value: wordpress - name: MYSQL_PASSWORD valueFrom: secretKeyRef: name: mysql-pass key: password ports: - containerPort: 3306 name: mysql volumeMounts: - name: mysql-persistent-storage mountPath: /var/lib/mysql resources: requests: memory: "1Gi" cpu: "500m" limits: memory: "2Gi" cpu: "1000m" livenessProbe: exec: command: - mysqladmin - ping - -h - localhost initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 5 readinessProbe: exec: command: - mysqladmin - ping - -h - localhost initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 1 volumes: - name: mysql-persistent-storage persistentVolumeClaim: claimName: mysql-pvc
Apply the MySQL configuration:
1 2 3 4 5 6 7 8 kubectl apply -f mysql-deployment.yaml kubectl get pods -n wordpress -w kubectl get all -n wordpress
Expected output:
1 2 3 4 5 6 7 8 NAME READY STATUS RESTARTS AGE pod/wordpress-mysql-7b4c9f5d-xk9j8 1/1 Running 0 2m NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/wordpress-mysql ClusterIP None <none> 3306/TCP 2m NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/wordpress-mysql 1/1 1 1 2m
Step 3: Verify Database Connectivity 1 2 3 4 5 6 7 8 kubectl run -it --rm mysql-client \ --image=mysql:8.0 \ --restart=Never \ -n wordpress \ -- mysql -h wordpress-mysql -u wordpress -p'YourSecurePassword123!' -e "SHOW DATABASES;"
Deploying WordPress Application Now that MySQL is running, let’s deploy the WordPress application.
WordPress files (themes, plugins, uploads) need to be shared across all pods:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 apiVersion: v1 kind: PersistentVolumeClaim metadata: name: wordpress-pvc namespace: wordpress labels: app: wordpress spec: accessModes: - ReadWriteMany resources: requests: storage: 50Gi storageClassName: nfs
Apply the PVC:
1 kubectl apply -f wordpress-pvc.yaml
Step 2: Deploy WordPress Application Create wordpress-deployment.yaml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 apiVersion: v1 kind: Service metadata: name: wordpress namespace: wordpress labels: app: wordpress spec: ports: - port: 80 targetPort: 80 protocol: TCP selector: app: wordpress tier: frontend type: ClusterIP --- apiVersion: apps/v1 kind: Deployment metadata: name: wordpress namespace: wordpress labels: app: wordpress spec: replicas: 2 selector: matchLabels: app: wordpress tier: frontend strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 template: metadata: labels: app: wordpress tier: frontend spec: initContainers: - name: volume-permissions image: busybox:latest command: ['sh' , '-c' , 'chown -R 33:33 /var/www/html' ] volumeMounts: - name: wordpress-persistent-storage mountPath: /var/www/html containers: - name: wordpress image: wordpress:6.4-php8.2-apache env: - name: WORDPRESS_DB_HOST value: wordpress-mysql - name: WORDPRESS_DB_USER value: wordpress - name: WORDPRESS_DB_PASSWORD valueFrom: secretKeyRef: name: mysql-pass key: password - name: WORDPRESS_DB_NAME value: wordpress - name: WORDPRESS_TABLE_PREFIX value: wp_ - name: WORDPRESS_DEBUG value: "0" - name: PHP_MEMORY_LIMIT value: 256M - name: PHP_MAX_EXECUTION_TIME value: "300" - name: PHP_UPLOAD_MAX_FILESIZE value: 64M - name: PHP_POST_MAX_SIZE value: 64M ports: - containerPort: 80 name: wordpress protocol: TCP volumeMounts: - name: wordpress-persistent-storage mountPath: /var/www/html resources: requests: memory: "512Mi" cpu: "250m" limits: memory: "1Gi" cpu: "500m" livenessProbe: httpGet: path: /wp-admin/install.php port: 80 initialDelaySeconds: 90 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 3 readinessProbe: httpGet: path: /wp-admin/install.php port: 80 initialDelaySeconds: 30 periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 3 volumes: - name: wordpress-persistent-storage persistentVolumeClaim: claimName: wordpress-pvc
Deploy WordPress:
1 2 3 4 5 6 7 8 kubectl apply -f wordpress-deployment.yaml kubectl get pods -n wordpress -w kubectl get all -n wordpress
Enable automatic scaling based on CPU usage:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: wordpress-hpa namespace: wordpress spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: wordpress minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70 - type: Resource resource: name: memory target: type: Utilization averageUtilization: 80 behavior: scaleUp: stabilizationWindowSeconds: 0 policies: - type: Percent value: 100 periodSeconds: 15 scaleDown: stabilizationWindowSeconds: 300 policies: - type: Pods value: 1 periodSeconds: 60
Apply the HPA:
1 2 3 4 kubectl apply -f wordpress-hpa.yaml kubectl get hpa -n wordpress
Setting Up Ingress and SSL Make WordPress accessible from the internet with HTTPS.
Step 1: Install cert-manager for SSL 1 2 3 4 5 6 7 8 9 helm repo add jetstack https://charts.jetstack.io helm repo update kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml kubectl get pods -n cert-manager
Step 2: Create ClusterIssuer for Let’s Encrypt 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-prod spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: [email protected] privateKeySecretRef: name: letsencrypt-prod solvers: - http01: ingress: class: nginx
Apply the issuer:
1 kubectl apply -f letsencrypt-issuer.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: wordpress-ingress namespace: wordpress annotations: cert-manager.io/cluster-issuer: "letsencrypt-prod" nginx.ingress.kubernetes.io/proxy-body-size: "64m" nginx.ingress.kubernetes.io/proxy-read-timeout: "300" nginx.ingress.kubernetes.io/proxy-send-timeout: "300" nginx.ingress.kubernetes.io/configuration-snippet: | add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; spec: ingressClassName: nginx tls: - hosts: - your-domain.com - www.your-domain.com secretName: wordpress-tls rules: - host: your-domain.com http: paths: - path: / pathType: Prefix backend: service: name: wordpress port: number: 80 - host: www.your-domain.com http: paths: - path: / pathType: Prefix backend: service: name: wordpress port: number: 80
Apply the ingress:
1 2 3 4 5 6 7 8 kubectl apply -f wordpress-ingress.yaml kubectl get certificate -n wordpress -w kubectl describe ingress wordpress-ingress -n wordpress
Step 4: Update DNS Records Point your domain to the ingress controller’s load balancer:
1 2 3 4 5 6 kubectl get svc -n ingress-nginx ingress-nginx-controller
Implementing Backup Strategy Protect your WordPress data with automated backups.
Step 1: Install Velero for Backup 1 2 3 4 5 6 7 8 9 10 11 12 13 wget https://github.com/vmware-tanzu/velero/releases/download/v1.12.0/velero-v1.12.0-linux-amd64.tar.gz tar -xvf velero-v1.12.0-linux-amd64.tar.gz sudo mv velero-v1.12.0-linux-amd64/velero /usr/local/bin/velero install \ --provider aws \ --plugins velero/velero-plugin-for-aws:v1.8.0 \ --bucket wordpress-backups \ --backup-location-config region=us-east-1 \ --snapshot-location-config region=us-east-1 \ --secret-file ./credentials-velero
Step 2: Create Backup Schedule 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 apiVersion: velero.io/v1 kind: Schedule metadata: name: wordpress-daily-backup namespace: velero spec: schedule: "0 2 * * *" template: includedNamespaces: - wordpress storageLocation: default volumeSnapshotLocations: - default ttl: 720h0m0s hooks: resources: - name: mysql-backup-hook includedNamespaces: - wordpress labelSelector: matchLabels: app: wordpress tier: mysql pre: - exec: container: mysql command: - /bin/bash - -c - mysqldump --all-databases > /tmp/backup.sql onError: Fail timeout: 3m
Apply the backup schedule:
1 2 3 4 5 6 7 8 9 10 kubectl apply -f wordpress-backup-schedule.yaml velero schedule get velero backup create wordpress-manual-backup --from-schedule wordpress-daily-backup velero backup describe wordpress-manual-backup
Step 3: Test Backup Restoration 1 2 3 4 5 6 7 8 velero backup get velero restore create --from-backup wordpress-daily-backup-20251228 velero restore describe wordpress-daily-backup-20251228
Implementing Redis Cache Add Redis for object caching:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 apiVersion: apps/v1 kind: Deployment metadata: name: wordpress-redis namespace: wordpress spec: replicas: 1 selector: matchLabels: app: redis template: metadata: labels: app: redis spec: containers: - name: redis image: redis:7-alpine ports: - containerPort: 6379 resources: requests: memory: "256Mi" cpu: "100m" limits: memory: "512Mi" cpu: "200m" volumeMounts: - name: redis-data mountPath: /data volumes: - name: redis-data emptyDir: {} --- apiVersion: v1 kind: Service metadata: name: wordpress-redis namespace: wordpress spec: ports: - port: 6379 targetPort: 6379 selector: app: redis
Deploy Redis:
1 kubectl apply -f redis-deployment.yaml
Update WordPress deployment to use Redis:
1 2 3 4 5 - name: WORDPRESS_REDIS_HOST value: wordpress-redis - name: WORDPRESS_REDIS_PORT value: "6379"
Configuring CDN Set up CloudFlare or similar CDN:
Point domain to CloudFlare nameservers
Enable SSL/TLS (Full mode)
Enable caching for static assets
Configure page rules for WordPress admin
Update WordPress to use CDN for static assets:
1 2 define ('WP_CONTENT_URL' , 'https://cdn.your-domain.com/wp-content' );
Security Hardening Step 1: Create Network Policies 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: wordpress-network-policy namespace: wordpress spec: podSelector: matchLabels: app: wordpress policyTypes: - Ingress - Egress ingress: - from: - namespaceSelector: matchLabels: name: ingress-nginx ports: - protocol: TCP port: 80 egress: - to: - namespaceSelector: {} podSelector: matchLabels: k8s-app: kube-dns ports: - protocol: UDP port: 53 - to: - podSelector: matchLabels: app: wordpress tier: mysql ports: - protocol: TCP port: 3306 - to: - podSelector: matchLabels: app: redis ports: - protocol: TCP port: 6379 - to: - namespaceSelector: {} ports: - protocol: TCP port: 443 - protocol: TCP port: 80
Apply network policies:
1 kubectl apply -f network-policy.yaml
1 2 3 4 5 6 7 8 9 apiVersion: v1 kind: Namespace metadata: name: wordpress labels: pod-security.kubernetes.io/enforce: restricted pod-security.kubernetes.io/audit: restricted pod-security.kubernetes.io/warn: restricted
Step 3: Implement Security Context Update WordPress deployment with security context:
1 2 3 4 5 6 7 8 9 securityContext: runAsNonRoot: true runAsUser: 33 fsGroup: 33 seccompProfile: type: RuntimeDefault capabilities: drop: - ALL
Monitoring and Observability Setting Up Prometheus and Grafana Install monitoring stack:
1 2 3 4 5 6 7 8 9 helm repo add prometheus-community https://prometheus-community.github.io/helm-charts helm repo update helm install monitoring prometheus-community/kube-prometheus-stack \ --namespace monitoring \ --create-namespace \ --set prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=false
Create WordPress ServiceMonitor 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: wordpress-monitor namespace: wordpress labels: app: wordpress spec: selector: matchLabels: app: wordpress endpoints: - port: wordpress path: /metrics interval: 30s
Access Grafana Dashboard 1 2 3 4 5 6 7 8 9 10 kubectl get secret -n monitoring monitoring-grafana \ -o jsonpath="{.data.admin-password}" | base64 --decode kubectl port-forward -n monitoring svc/monitoring-grafana 3000:80
Import WordPress monitoring dashboard (ID: 13459)
Troubleshooting Common Issues Issue 1: WordPress Pods Failing to Start Symptoms:
1 2 kubectl get pods -n wordpress
Diagnosis:
1 2 3 4 5 kubectl logs -n wordpress <wordpress-pod-name> kubectl describe pod -n wordpress <wordpress-pod-name>
Common Causes and Solutions:
Issue
Cause
Solution
Permission denied
Wrong file ownership
Add initContainer to fix permissions
Cannot connect to MySQL
MySQL not ready
Ensure MySQL is running first
OOMKilled
Memory limit too low
Increase memory limits
Issue 2: File Upload Failures Symptoms: File uploads fail with “413 Request Entity Too Large”
Solution:
1 2 3 4 5 kubectl annotate ingress wordpress-ingress \ -n wordpress \ nginx.ingress.kubernetes.io/proxy-body-size=64m \ --overwrite
Diagnosis:
1 2 3 4 5 kubectl top pods -n wordpress kubectl get hpa -n wordpress
Solutions:
Enable Redis caching
Increase replica count
Lower HPA CPU threshold
Add CDN for static assets
Optimize database queries
Issue 4: SSL Certificate Not Issued Diagnosis:
1 2 3 4 5 kubectl describe certificate wordpress-tls -n wordpress kubectl logs -n cert-manager deploy/cert-manager
Common Solutions:
Verify DNS records point to correct IP
Check ClusterIssuer email address
Ensure port 80 is accessible for ACME challenge
Check rate limits (Let’s Encrypt has limits)
Production Best Practices Resource Management 1 2 3 4 5 6 7 8 resources: requests: memory: "512Mi" cpu: "250m" limits: memory: "1Gi" cpu: "500m"
Database Connection Pooling Add to WordPress environment:
1 2 3 4 - name: WORDPRESS_DB_CHARSET value: utf8mb4 - name: WORDPRESS_DB_COLLATE value: utf8mb4_unicode_ci
Advanced Configuration Multi-Site WordPress Setup 1 2 3 4 5 6 7 8 9 10 - name: WORDPRESS_CONFIG_EXTRA value: | define('WP_ALLOW_MULTISITE', true); define('MULTISITE', true); define('SUBDOMAIN_INSTALL', false); define('DOMAIN_CURRENT_SITE', 'your-domain.com'); define('PATH_CURRENT_SITE', '/'); define('SITE_ID_CURRENT_SITE', 1); define('BLOG_ID_CURRENT_SITE', 1);
Blue-Green Deployment Strategy 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 apiVersion: v1 kind: Service metadata: name: wordpress-blue namespace: wordpress spec: selector: app: wordpress version: blue ports: - port: 80 --- apiVersion: v1 kind: Service metadata: name: wordpress-green namespace: wordpress spec: selector: app: wordpress version: green ports: - port: 80
Switch traffic by updating ingress backend:
1 2 3 4 kubectl patch ingress wordpress-ingress -n wordpress \ --type ='json' \ -p='[{"op": "replace", "path": "/spec/rules/0/http/paths/0/backend/service/name", "value":"wordpress-green"}]'
Cleanup and Teardown Complete Cleanup Procedure 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 kubectl delete namespace wordpress kubectl delete pv --selector=app=wordpress helm uninstall monitoring -n monitoring kubectl delete namespace monitoring kubectl delete -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml velero uninstall
Selective Cleanup 1 2 3 4 5 6 7 8 9 kubectl delete deployment wordpress -n wordpress kubectl delete deployment wordpress-mysql -n wordpress kubectl delete pvc mysql-pvc -n wordpress velero backup get