Panduan Lengkap Deploy WordPress + Redis + MySQL di Kubernetes dengan SSL Cloudflare

Mau bikin website WordPress super cepat, aman, dan bisa di-scale di Kubernetes? 💡 Artikel ini akan membahas langkah demi langkah bagaimana cara deploy WordPress + MySQL + Redis di atas Kubernetes, lengkap dengan SSL Let’s Encrypt (DNS-01 via Cloudflare). 🔒✨
🛠️ 1. Persiapan Cluster Kubernetes
Sebelum mulai, pastikan sudah punya:
- Cluster Kubernetes minimal 2 node (1 master + 1 worker).
- Ingress Nginx Controller sudah ter-install.
- Cert-Manager terpasang (untuk auto SSL).
- Domain aktif di Cloudflare dengan mode proxy OFF (hanya DNS).
👉 Server specs pada contoh ini:
- 12 vCPU + 25 GB RAM + 50 GB Disk
- Sangat cukup untuk handle traffic besar.
📦 2. Struktur Stack Kubernetes
Stack final terdiri dari komponen berikut:
- Namespace
masdika
→ isolasi semua resource. - PersistentVolume (PV) & PersistentVolumeClaim (PVC) untuk MySQL, Redis, dan WordPress.
- MySQL (StatefulSet) untuk database.
- Redis (StatefulSet) untuk caching.
- WordPress (Deployment) sebagai CMS utama.
- Ingress dengan auto SSL dari Let’s Encrypt via Cloudflare.
- HPA (Horizontal Pod Autoscaler) → tetap 1 replika karena PVC RWO.
🔑 3. Konfigurasi Secrets & Cloudflare API Token
Untuk validasi DNS-01, kita perlu API Token Cloudflare:
apiVersion: v1
kind: Secret
metadata:
name: cloudflare-api-token-secret
namespace: cert-manager
stringData:
api-token: "TOKEN_CLOUDFLARE_ANDA"
Token ini digunakan oleh cert-manager untuk menulis record TXT otomatis ke DNS Cloudflare saat request SSL.
🐬 4. Deploy MySQL (Database)
MySQL dijalankan dalam StatefulSet agar data tetap persist di PVC.
- Default database:
wordpress
- User:
wpuser
- Password:
WpUserP4ss!
Semua environment variable diinject dari Secret agar lebih aman. 🔐
⚡ 5. Deploy Redis (Caching)
Redis digunakan untuk Object Cache di WordPress.
- Password Redis disimpan di
wp-secrets
. - Redis berjalan dengan persistence (
appendonly yes
).
👉 WordPress terhubung ke Redis via Predis client:
define('WP_REDIS_HOST','redis');
define('WP_REDIS_PORT',6379);
define('WP_REDIS_CLIENT','predis');
define('WP_REDIS_PASSWORD', getenv('REDIS_PASSWORD') ?: '');
Hasilnya: query database bisa di-cache, mempercepat loading WordPress 🚀
🌐 6. Deploy WordPress (PHP + Apache)
WordPress dijalankan dengan image resmi wordpress:php8.2-apache
.
Fitur tambahan:
- Auto set URL →
https://www.masdika.biz.id
- Custom PHP limits → upload 1GB, memory limit 2GB.
- Init container untuk fix permission file WordPress.
- Redis Cache otomatis terhubung.
🔒 7. Ingress + SSL Auto Renew
Ingress mengatur domain + SSL:
- Domain root
masdika.biz.id
redirect kewww.masdika.biz.id
. - SSL otomatis via cert-manager + ClusterIssuer Let’s Encrypt (DNS-01 Cloudflare).
- Upload size diatur
1024m
via annotation Nginx.
Hasilnya: Website WordPress bisa diakses aman via HTTPS ✅
📈 8. Horizontal Pod Autoscaler (HPA)
Karena PVC hostPath hanya bisa 1 replika (RWO), maka HPA dibatasi ke maxReplicas: 1
. Namun, resource besar server membuat 1 pod sudah sangat powerful.
Jika ingin scale-out lebih banyak pod WordPress:
- Gunakan storage RWX (mis. NFS, CephFS, atau Longhorn).
- Atur HPA maxReplicas ke 5–10 pod sesuai kebutuhan.
📜 9. Final YAML Lengkap
Berikut file YAML lengkap (tinggal apply saja):
# =========================================================
# masdika.yaml
# Stack lengkap WordPress + MySQL + Redis + Ingress (DNS-01 Cloudflare)
# Redis FIX: pakai env REDIS_* + Predis, password dari REDIS_PASSWORD
# =========================================================
# ---------- Namespace ----------
apiVersion: v1
kind: Namespace
metadata:
name: masdika
---
# ---------- PV hostPath ----------
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-mysql-20gi
spec:
capacity: { storage: 20Gi }
accessModes: ["ReadWriteOnce"]
persistentVolumeReclaimPolicy: Retain
hostPath: { path: /srv/k8s/mysql, type: DirectoryOrCreate }
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-redis-5gi
spec:
capacity: { storage: 5Gi }
accessModes: ["ReadWriteOnce"]
persistentVolumeReclaimPolicy: Retain
hostPath: { path: /srv/k8s/redis, type: DirectoryOrCreate }
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-wp-20gi
spec:
capacity: { storage: 20Gi }
accessModes: ["ReadWriteOnce"]
persistentVolumeReclaimPolicy: Retain
hostPath: { path: /srv/k8s/wp-content, type: DirectoryOrCreate }
# ---------- Secret Cloudflare API Token (namespace: cert-manager) ----------
# (Biarkan seperti ini jika token sama. Jika sudah dibuat sebelumnya dan tak ingin overwrite, hapus blok ini.)
---
apiVersion: v1
kind: Secret
metadata:
name: cloudflare-api-token-secret
namespace: cert-manager
type: Opaque
stringData:
api-token: "9Lxxr8Gq4Zts8yD_1Cff_iVf60OknqENuQ_whndt"
# ---------- ClusterIssuer Let's Encrypt (DNS-01 Cloudflare) ----------
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
email: [email protected]
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-prod-private-key
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token-secret
key: api-token
# ---------- Secrets & Config ----------
---
apiVersion: v1
kind: Secret
metadata:
name: wp-secrets
namespace: masdika
type: Opaque
stringData:
MYSQL_ROOT_PASSWORD: "SuperRootP4ss!"
MYSQL_DATABASE: "wordpress"
MYSQL_USER: "wpuser"
MYSQL_PASSWORD: "WpUserP4ss!"
WORDPRESS_ADMIN_USER: "admin"
WORDPRESS_ADMIN_PASSWORD: "AdminP4ss!"
WORDPRESS_ADMIN_EMAIL: "[email protected]"
REDIS_PASSWORD: "RedisP4ss!"
---
apiVersion: v1
kind: ConfigMap
metadata:
name: php-custom-ini
namespace: masdika
data:
zzz-custom.ini: |
file_uploads = On
upload_max_filesize = 1024M
post_max_size = 1024M
memory_limit = 2048M
max_execution_time = 300
# ---------- PVC ----------
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-data
namespace: masdika
spec:
accessModes: ["ReadWriteOnce"]
resources: { requests: { storage: 20Gi } }
volumeName: pv-mysql-20gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: redis-data
namespace: masdika
spec:
accessModes: ["ReadWriteOnce"]
resources: { requests: { storage: 5Gi } }
volumeName: pv-redis-5gi
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: wp-content
namespace: masdika
spec:
accessModes: ["ReadWriteOnce"]
resources: { requests: { storage: 20Gi } }
volumeName: pv-wp-20gi
# ---------- MySQL ----------
---
apiVersion: v1
kind: Service
metadata:
name: mysql
namespace: masdika
labels: { app: mysql }
spec:
clusterIP: None
ports: [{ port: 3306, name: mysql }]
selector: { app: mysql }
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
namespace: masdika
spec:
serviceName: mysql
replicas: 1
selector: { matchLabels: { app: mysql } }
template:
metadata: { labels: { app: mysql } }
spec:
containers:
- name: mysql
image: mysql:8.0
env:
- { name: MYSQL_ROOT_PASSWORD, valueFrom: { secretKeyRef: { name: wp-secrets, key: MYSQL_ROOT_PASSWORD } } }
- { name: MYSQL_DATABASE, valueFrom: { secretKeyRef: { name: wp-secrets, key: MYSQL_DATABASE } } }
- { name: MYSQL_USER, valueFrom: { secretKeyRef: { name: wp-secrets, key: MYSQL_USER } } }
- { name: MYSQL_PASSWORD, valueFrom: { secretKeyRef: { name: wp-secrets, key: MYSQL_PASSWORD } } }
args: ["--default-authentication-plugin=mysql_native_password","--innodb-buffer-pool-size=1G"]
ports: [{ containerPort: 3306, name: mysql }]
volumeMounts: [{ name: mysql-data, mountPath: /var/lib/mysql }]
volumes:
- { name: mysql-data, persistentVolumeClaim: { claimName: mysql-data } }
# ---------- Redis ----------
---
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: masdika
labels: { app: redis }
spec:
ports: [{ port: 6379, name: redis }]
selector: { app: redis }
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis
namespace: masdika
spec:
serviceName: redis
replicas: 1
selector: { matchLabels: { app: redis } }
template:
metadata: { labels: { app: redis } }
spec:
containers:
- name: redis
image: redis:7
env:
- { name: REDIS_PASSWORD, valueFrom: { secretKeyRef: { name: wp-secrets, key: REDIS_PASSWORD } } }
command: ["/bin/sh","-lc"]
args: ["exec redis-server --appendonly yes --requirepass \"$REDIS_PASSWORD\""]
ports: [{ containerPort: 6379, name: redis }]
volumeMounts: [{ name: redis-storage, mountPath: /data }]
volumes:
- { name: redis-storage, persistentVolumeClaim: { claimName: redis-data } }
# ---------- WordPress (Redis FIX: REDIS_* + Predis) ----------
---
apiVersion: v1
kind: Service
metadata:
name: wordpress
namespace: masdika
labels: { app: wordpress }
spec:
ports:
- { port: 80, targetPort: 80, name: http }
selector: { app: wordpress }
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: wordpress
namespace: masdika
spec:
replicas: 1
strategy: { type: Recreate } # PVC hostPath = RWO → 1 pod
selector: { matchLabels: { app: wordpress } }
template:
metadata: { labels: { app: wordpress } }
spec:
securityContext:
fsGroup: 33
fsGroupChangePolicy: OnRootMismatch
initContainers:
- name: fix-perms
image: busybox:1.36
command: ["sh","-c","chown -R 33:33 /var/www/html || true"]
volumeMounts:
- { name: wp-content, mountPath: /var/www/html }
containers:
- name: wordpress
image: wordpress:php8.2-apache
imagePullPolicy: IfNotPresent
env:
# DB
- { name: WORDPRESS_DB_HOST, value: "mysql:3306" }
- { name: WORDPRESS_DB_NAME, valueFrom: { secretKeyRef: { name: wp-secrets, key: MYSQL_DATABASE } } }
- { name: WORDPRESS_DB_USER, valueFrom: { secretKeyRef: { name: wp-secrets, key: MYSQL_USER } } }
- { name: WORDPRESS_DB_PASSWORD, valueFrom: { secretKeyRef: { name: wp-secrets, key: MYSQL_PASSWORD } } }
# Redis (ENV standar plugin)
- { name: REDIS_HOST, value: "redis" }
- { name: REDIS_PORT, value: "6379" }
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: wp-secrets
key: REDIS_PASSWORD
# Extra wp-config: aktifkan Predis & pakai REDIS_PASSWORD
- name: WORDPRESS_CONFIG_EXTRA
value: |
define('WP_HOME','https://www.masdika.biz.id');
define('WP_SITEURL','https://www.masdika.biz.id');
define('WP_MEMORY_LIMIT','512M');
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') { $_SERVER['HTTPS'] = 'on'; }
define('WP_REDIS_HOST','redis');
define('WP_REDIS_PORT',6379);
define('WP_REDIS_CLIENT','predis');
define('WP_REDIS_PASSWORD', getenv('REDIS_PASSWORD') ?: '' );
define('WP_CACHE_KEY_SALT','masdika_');
ports: [{ containerPort: 80, name: http }]
readinessProbe: { httpGet: { path: "/", port: http }, initialDelaySeconds: 30, periodSeconds: 10, failureThreshold: 12 }
livenessProbe: { httpGet: { path: "/", port: http }, initialDelaySeconds: 90, periodSeconds: 10, failureThreshold: 6 }
resources:
requests: { cpu: "500m", memory: "1Gi" }
limits: { cpu: "3", memory: "6Gi" }
volumeMounts:
- { name: wp-content, mountPath: /var/www/html }
- { name: phpini, mountPath: /usr/local/etc/php/conf.d/zzz-custom.ini, subPath: zzz-custom.ini }
envFrom: [{ secretRef: { name: wp-secrets } }]
volumes:
- { name: wp-content, persistentVolumeClaim: { claimName: wp-content } }
- { name: phpini, configMap: { name: php-custom-ini } }
# ---------- Ingress (TLS root+www, redirect → www) ----------
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: wordpress
namespace: masdika
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/from-to-www-redirect: "true"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "1024m"
spec:
ingressClassName: nginx
tls:
- hosts:
- masdika.biz.id
- www.masdika.biz.id
secretName: masdika-tls
rules:
- host: www.masdika.biz.id
http:
paths:
- path: /
pathType: Prefix
backend:
service: { name: wordpress, port: { number: 80 } }
# ---------- Certificate eksplisit ----------
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: masdika-tls
namespace: masdika
spec:
secretName: masdika-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- masdika.biz.id
- www.masdika.biz.id
# ---------- HPA (max 1 karena PVC RWO) ----------
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: wordpress
namespace: masdika
spec:
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: wordpress }
minReplicas: 1
maxReplicas: 1
metrics:
- type: Resource
resource: { name: cpu, target: { type: Utilization, averageUtilization: 60 } }
- type: Resource
resource: { name: memory, target: { type: Utilization, averageUtilization: 70 } }
🎯 10. Hasil Akhir



✅ WordPress online di https://www.masdika.biz.id
✅ SSL otomatis dari Let’s Encrypt via Cloudflare
✅ Redis Object Cache aktif → performa meningkat
✅ MySQL persistent & secure
✅ Stack mudah di-maintain (1 file YAML saja)
✨ Kesimpulan
Dengan setup ini, kita punya WordPress super cepat, aman, scalable, dan hemat biaya di atas Kubernetes. Semua resource dipisahkan dalam namespace, SSL auto renew, Redis cache aktif, dan siap dipakai untuk production. 💪🔥
👉 Next step: bisa ditambah monitoring (Prometheus + Grafana), backup MySQL otomatis,

🚀 Apa itu K3s? K3s adalah distribusi Kubernetes ringan dari Rancher yang dirancang untuk mempermudah proses instalasi dan penggunaan Kubernetes….

Buat kamu yang lagi Deploy aplikasi dengan Next.js dan pengen jalan di Kubernetes dengan domain custom + SSL otomatis, artikel…
Thanks for sharing mas
sama sama mas
Mantap mas