Deploying a Highly Available WordPress Application on Kubernetes with Rolling Updates, HPA, and Ingress
This guide walks through deploying WordPress on Kubernetes with persistent storage, multi‑replica deployments, high‑availability configurations, health checks, pod anti‑affinity, PodDisruptionBudgets, QoS settings, rolling updates, automatic scaling via HPA, secure Secrets, and HTTPS exposure through Ingress, providing complete YAML manifests and command‑line steps.
In this tutorial we use a WordPress example to demonstrate how to achieve high availability, zero‑downtime rolling updates, persistent data, automatic scaling, and HTTPS access for a production‑grade application deployed on Kubernetes.
Principle
WordPress runs on PHP and MySQL, so we need a Docker image for WordPress (official wordpress:5.3.2-apache ) and a MySQL image. Both containers can be placed in the same Pod to share the network namespace, but for scalability we later separate them.
apiVersion: v1
kind: Namespace
metadata:
name: kube-exampleWe then create a Deployment manifest for WordPress and MySQL:
apiVersion: apps/v1
kind: Deployment
metadata:
name: wordpress
namespace: kube-example
labels:
app: wordpress
spec:
selector:
matchLabels:
app: wordpress
template:
metadata:
labels:
app: wordpress
spec:
containers:
- name: wordpress
image: wordpress:5.3.2-apache
ports:
- containerPort: 80
name: wdport
env:
- name: WORDPRESS_DB_HOST
value: localhost:3306
- name: WORDPRESS_DB_USER
value: wordpress
- name: WORDPRESS_DB_PASSWORD
value: wordpress
- name: mysql
image: mysql:5.7
imagePullPolicy: IfNotPresent
args:
- --default_authentication_plugin=mysql_native_password
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
ports:
- containerPort: 3306
name: dbport
env:
- name: MYSQL_ROOT_PASSWORD
value: rootPassW0rd
- name: MYSQL_DATABASE
value: wordpress
- name: MYSQL_USER
value: wordpress
- name: MYSQL_PASSWORD
value: wordpressBecause the two containers share a network namespace, the WordPress container can reach MySQL via localhost:3306 . To expose the service we create a NodePort Service:
apiVersion: v1
kind: Service
metadata:
name: wordpress
namespace: kube-example
spec:
selector:
app: wordpress
type: NodePort
ports:
- name: web
port: 80
targetPort: wdportAfter applying the manifests with kubectl apply -f , the Pods start and can be accessed through http://<nodeIP>:30892 . However, this single‑replica setup has a single‑point‑of‑failure and does not scale.
High Availability
We split WordPress and MySQL into separate Deployments so that WordPress can be scaled while MySQL remains a single instance (or later a StatefulSet). The MySQL Service is created to give WordPress a stable DNS name:
apiVersion: v1
kind: Service
metadata:
name: wordpress-mysql
namespace: kube-example
labels:
app: wordpress
spec:
ports:
- port: 3306
targetPort: dbport
selector:
app: wordpress
tier: mysqlThe WordPress Deployment is updated to use replicas: 3 and to point to the MySQL Service DNS name wordpress-mysql:3306 :
apiVersion: apps/v1
kind: Deployment
metadata:
name: wordpress
namespace: kube-example
labels:
app: wordpress
tier: frontend
spec:
replicas: 3
selector:
matchLabels:
app: wordpress
tier: frontend
template:
metadata:
labels:
app: wordpress
tier: frontend
spec:
containers:
- name: wordpress
image: wordpress:5.3.2-apache
ports:
- containerPort: 80
name: wdport
env:
- name: WORDPRESS_DB_HOST
value: wordpress-mysql:3306
- name: WORDPRESS_DB_USER
value: wordpress
- name: WORDPRESS_DB_PASSWORD
value: wordpressNow three WordPress Pods run on different nodes, providing redundancy.
Stability
To avoid single‑node failures we add pod anti‑affinity so that replicas are spread across hosts:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
podAffinityTerm:
topologyKey: kubernetes.io/hostname
labelSelector:
matchExpressions:
- key: app
operator: In
values:
- wordpressWe also define a PodDisruptionBudget to ensure at most one replica is unavailable during voluntary disruptions:
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
name: wordpress-pdb
namespace: kube-example
spec:
maxUnavailable: 1
selector:
matchLabels:
app: wordpress
tier: frontendReadiness probes are added so that a Pod is removed from the Service only after it is truly ready to receive traffic:
readinessProbe:
tcpSocket:
port: 80
initialDelaySeconds: 5
periodSeconds: 5QoS and Resource Management
Kubernetes classifies Pods into Guaranteed, Burstable, and Best‑Effort based on CPU/memory requests and limits. For WordPress we set explicit limits and requests to achieve a Guaranteed QoS:
resources:
limits:
cpu: 200m
memory: 100Mi
requests:
cpu: 200m
memory: 100MiThese settings influence OOM scores and eviction order.
Rolling Updates
The Deployment controller performs rolling updates by default. We can fine‑tune the strategy:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0To achieve true zero‑downtime we add a preStop hook that sleeps long enough for the Service endpoints to be updated before the container exits:
lifecycle:
preStop:
exec:
command: ["/bin/bash", "-c", "sleep 20"]Horizontal Pod Autoscaling (HPA)
We enable automatic scaling based on CPU usage:
kubectl autoscale deployment wordpress --namespace kube-example --cpu-percent=20 --min=3 --max=6During a load test with Fortio the replica count grows to six and shrinks back after the load subsides.
Security
Sensitive data such as the database password is stored in a Kubernetes Secret and referenced in the Deployment:
kubectl create secret generic wordpress-db-pwd --from-literal=dbpwd=wordpress -n kube-example env:
- name: WORDPRESS_DB_PASSWORD
valueFrom:
secretKeyRef:
name: wordpress-db-pwd
key: dbpwdPersistence
We create a PVC for MySQL using the rook-ceph-block StorageClass and a PVC for WordPress using a CephFS StorageClass ( csi-cephfs ) that supports ReadWriteMany for multiple replicas.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pvc
namespace: kube-example
spec:
storageClassName: rook-ceph-block
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: wordpress-pvc
namespace: kube-example
spec:
storageClassName: csi-cephfs
accessModes:
- ReadWriteMany
resources:
requests:
storage: 2GiThe Deployments mount these PVCs at /var/www/html (WordPress) and /var/lib/mysql (MySQL).
Ingress (HTTPS)
For production exposure we replace the NodePort with a Traefik IngressRoute that terminates TLS using ACME and redirects HTTP to HTTPS:
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: wordpress-https
namespace: kube-example
spec:
entryPoints:
- websecure
routes:
- match: Host(`wordpress.qikqiak.com`)
kind: Rule
services:
- name: wordpress
port: 80
tls:
certResolver: ali
domains:
- main: "*.qikqiak.com"
---
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: redirect-https
namespace: kube-example
spec:
redirectScheme:
scheme: https
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: wordpress-http
namespace: kube-example
spec:
entryPoints:
- web
routes:
- match: Host(`wordpress.qikqiak.com`)
kind: Rule
services:
- name: wordpress
port: 80
middlewares:
- name: redirect-httpsAfter applying these resources, the domain wordpress.qikqiak.com resolves to a secure WordPress site, completing a production‑ready deployment.
DevOps Cloud Academy
Exploring industry DevOps practices and technical expertise.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.