Top Tags

Simple WordPress setup with MySQL on K3s

How to deploy WordPress with MySQL on K3s with PVC and expose it to LAN by LoadBalancer

Simple setup. 1 replica WordPress + 1 replica MySQL with PVC on K3s and expose it to LAN by LoadBalancer.

Overview

This guide demonstrates deploying a production-ready WordPress application with MySQL database backend on K3s (lightweight Kubernetes distribution). The setup leverages Kubernetes PersistentVolumes (PV) and PersistentVolumeClaims (PVC) for stateful data persistence, ensuring data survives pod restarts and rescheduling events.

Architecture Components

  • K3s Cluster: Lightweight Kubernetes distribution optimized for edge and resource-constrained environments
  • MySQL 8.0: Relational database providing data persistence with InnoDB storage engine
  • WordPress: Web application tier serving content over HTTP/HTTPS
  • PersistentVolumes: Cluster-level storage resources managed by K3s
  • MetalLB LoadBalancer: Provides external IP allocation for service exposure to LAN
  • Network Segmentation: Pod-to-pod communication via Kubernetes DNS

Storage Architecture

PersistentVolumeClaims abstract the underlying storage implementation from pods. K3s supports multiple storage backends:

  • Local Path Storage (default): Uses node's local filesystem - suitable for single-node clusters
  • Network Storage: NFS, iSCSI, or cloud storage for multi-node deployments
  • Storage Classes: Define provisioning policies and performance characteristics

Create deployment MySQL

bash
1nano mysql.yaml
yaml
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 name: mysql
5 labels:
6 app: mysql
7spec:
8 replicas: 1
9 selector:
10 matchLabels:
11 app: mysql
12 template:
13 metadata:
14 labels:
15 app: mysql
16 spec:
17 containers:
18 - name: mysql
19 image: mysql:8
20 env:
21 - name: MYSQL_ROOT_PASSWORD
22 value: rootpassword
23 - name: MYSQL_DATABASE
24 value: wordpress
25 - name: MYSQL_USER
26 value: wpuser
27 - name: MYSQL_PASSWORD
28 value: wppassword
29 ports:
30 - containerPort: 3306
31 volumeMounts:
32 - name: mysql-pvc
33 mountPath: /var/lib/mysql
34 volumes:
35 - name: mysql-pvc
36 persistentVolumeClaim:
37 claimName: mysql-pvc
38---
39apiVersion: v1
40kind: PersistentVolumeClaim
41metadata:
42 name: mysql-pvc
43spec:
44 accessModes:
45 - ReadWriteOnce
46 resources:
47 requests:
48 storage: 10Gi
49---
50apiVersion: v1
51kind: Service
52metadata:
53 name: mysql
54spec:
55 type: LoadBalancer
56 ports:
57 - port: 3306
58 targetPort: 3306
59 selector:
60 app: mysql

Create deployment WordPress

bash
1nano wp.yaml
yaml
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 name: wordpress
5 labels:
6 app: wordpress
7spec:
8 replicas: 1
9 selector:
10 matchLabels:
11 app: wordpress
12 template:
13 metadata:
14 labels:
15 app: wordpress
16 spec:
17 containers:
18 - name: wordpress
19 image: wordpress:latest
20 ports:
21 - containerPort: 80
22 env:
23 - name: WORDPRESS_DB_HOST
24 value: mysql
25 - name: WORDPRESS_DB_USER
26 value: wpuser
27 - name: WORDPRESS_DB_PASSWORD
28 value: wppassword
29 - name: WORDPRESS_DB_NAME
30 value: wordpress
31 volumeMounts:
32 - name: wordpress-pvc
33 mountPath: /var/www/html
34 volumes:
35 - name: wordpress-pvc
36 persistentVolumeClaim:
37 claimName: wordpress-pvc
38---
39apiVersion: v1
40kind: Service
41metadata:
42 name: wordpress
43spec:
44 type: LoadBalancer
45 selector:
46 app: wordpress
47 ports:
48 - port: 80
49 targetPort: 80

Create PVC for WordPress

bash
1nano pvc.yaml
yaml
1apiVersion: v1
2kind: PersistentVolumeClaim
3metadata:
4 name: wordpress-pvc
5spec:
6 accessModes:
7 - ReadWriteOnce
8 resources:
9 requests:
10 storage: 10Gi

Deploy all resources

bash
1kubectl apply -f pvc.yaml
2kubectl apply -f mysql.yaml
3kubectl apply -f wp.yaml

Verification and Troubleshooting

Verify Deployment Status

bash
1# Check pod status and readiness
2kubectl get pods -o wide
3
4# Get detailed pod information
5kubectl describe pod mysql-<pod-id>
6kubectl describe pod wordpress-<pod-id>
7
8# Monitor pod logs for startup errors
9kubectl logs mysql-<pod-id>
10kubectl logs wordpress-<pod-id> -f

Verify PVC Binding

bash
1# List all PersistentVolumeClaims
2kubectl get pvc
3
4# Inspect PVC details
5kubectl describe pvc mysql-pvc
6kubectl describe pvc wordpress-pvc
7
8# Check PersistentVolume status
9kubectl get pv

Check Service and LoadBalancer

bash
1# List services and external IPs
2kubectl get svc
3
4# Watch service assignment of external IP
5kubectl get svc -w
6
7# Test MySQL connectivity from within cluster
8kubectl exec -it deployment/wordpress -- mysql -h mysql -u wpuser -p wordpress -e "SELECT 1;"

Technical Deep Dive

Kubernetes Storage Fundamentals

PersistentVolume (PV) vs PersistentVolumeClaim (PVC):

  • PV: Cluster-level storage resource with lifecycle independent of pods
  • PVC: Pod-level storage request that binds to available PVs
  • Binding: Kubernetes matches PVC requests with suitable PVs based on:
    • Storage capacity
    • Access modes (ReadWriteOnce, ReadOnlyMany, ReadWriteMany)
    • StorageClass matching
    • Label selectors

Access Modes Explanation:

  • ReadWriteOnce: Single pod read/write access (node-level exclusivity)
  • ReadOnlyMany: Multiple pods read-only access
  • ReadWriteMany: Multiple pods read/write access (requires network storage)

MySQL Container Configuration

Environment Variables and Initialization

The MySQL container initializes through environment variables on first startup:

  • MYSQL_ROOT_PASSWORD: Sets root user password (critical for security)
  • MYSQL_DATABASE: Pre-creates specified database during initialization
  • MYSQL_USER: Creates non-root user with permissions on MYSQL_DATABASE
  • MYSQL_PASSWORD: Password for the MYSQL_USER account

Security Considerations:

  • Root password stored in plain YAML (development only)
  • For production, use Kubernetes Secrets:
yaml
1apiVersion: v1
2kind: Secret
3metadata:
4 name: mysql-credentials
5type: Opaque
6stringData:
7 root-password: ${SECURE_ROOT_PASSWORD}
8 user-password: ${SECURE_USER_PASSWORD}
9---
10# Reference in deployment:
11- name: MYSQL_ROOT_PASSWORD
12 valueFrom:
13 secretKeyRef:
14 name: mysql-credentials
15 key: root-password

MySQL Storage Path

  • Container Mount Point: /var/lib/mysql
  • Data Files: InnoDB tablespaces, binary logs, replication metadata
  • Performance: PVC mount point determines I/O performance characteristics
  • Recovery: Data persists across container restarts via PVC binding

InnoDB Storage Engine Specifics

MySQL 8.0 uses InnoDB by default, providing:

  • ACID compliance for transactional consistency
  • Row-level locking for concurrent access
  • Crash recovery mechanisms
  • Doublewrite buffer for data integrity

Buffer Pool Configuration (important for K3s resource constraints):

mysql
1-- Check current settings
2SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
3
4-- InnoDB uses ~75-80% of available memory for buffer pool
5-- For K3s nodes with 2GB available: set to ~1.5GB

WordPress Application Configuration

Initialization Flow

WordPress requires database connectivity during startup:

  1. Image Pull: wordpress:latest (Apache + PHP + WordPress)
  2. Entrypoint Script: Detects wp-config.php missing, generates from environment
  3. Database Connection: Attempts MySQL connection via WORDPRESS_DB_HOST
  4. WordPress Setup: Creates tables and initializes content
  5. Service Ready: Serves HTTP requests on port 80

Environment Variable Dependencies

yaml
1WORDPRESS_DB_HOST: mysql # Must match MySQL Service name (DNS resolvable)
2WORDPRESS_DB_USER: wpuser # Must exist in MySQL with database permissions
3WORDPRESS_DB_PASSWORD: wppassword # Credentials match MySQL setup
4WORDPRESS_DB_NAME: wordpress # Must pre-exist in MySQL

DNS Resolution: WORDPRESS_DB_HOST: mysql resolves via Kubernetes CoreDNS to MySQL Service ClusterIP address.

PHP-FPM and Apache Configuration

  • Web Server: Apache 2.4 with mod_php
  • PHP Version: Varies with image tag (latest ≈ PHP 8.2+)
  • Max Upload Size: Default 64MB, configurable via PHP_INI environment variables
  • Default Document Root: /var/www/html

Networking Architecture

Service Discovery

WordPress Pod (10.42.x.x) ↓ Service mysql:3306 ↓ DNS Resolution (CoreDNS) ↓ MySQL Pod (10.42.x.x)

LoadBalancer Service Integration

MetalLB assigns external IPs from configured address pools:

bash
1# Check LoadBalancer status
2kubectl get svc wordpress -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
3
4# Access WordPress via external IP
5curl http://<EXTERNAL_IP>

Network Policies (Optional Enhancement)

yaml
1apiVersion: networking.k8s.io/v1
2kind: NetworkPolicy
3metadata:
4 name: wordpress-to-mysql
5spec:
6 podSelector:
7 matchLabels:
8 app: wordpress
9 policyTypes:
10 - Egress
11 egress:
12 - to:
13 - podSelector:
14 matchLabels:
15 app: mysql
16 ports:
17 - protocol: TCP
18 port: 3306
19 - to:
20 - namespaceSelector: {}
21 ports:
22 - protocol: TCP
23 port: 53 # DNS
24 - protocol: UDP
25 port: 53

Performance Optimization

Resource Requests and Limits

Enhance deployments with resource specifications:

yaml
1resources:
2 requests:
3 memory: "256Mi"
4 cpu: "250m"
5 limits:
6 memory: "512Mi"
7 cpu: "500m"

MySQL Memory Tuning:

  • Buffer Pool: 50-75% of container memory limit
  • Maximum Connections: Scale with available memory (~1MB per connection)
  • Sort Buffer: Optimize for query performance

Storage Performance Metrics

K3s local storage performance depends on:

  • Disk Type: SSD (optimal) vs HDD (slower)
  • Filesystem: ext4, XFS (journal modes affect write performance)
  • Mount Options: noatime, nodiratime can improve performance

Monitor using:

bash
1kubectl top pod <pod-name>
2kubectl top node

Scaling Considerations

Current limitations of this single-replica setup:

  • Single Failure Point: No MySQL replication/failover
  • Write Bottleneck: All WordPress instances depend on single MySQL
  • Storage Bottleneck: Single PVC may saturate disk I/O

Production improvements:

  • MySQL Primary-Secondary replication
  • WordPress horizontal pod autoscaling with shared volume or object storage
  • Read replicas with read-only WordPress instances

Monitoring and Maintenance

Health Checks Configuration

Add liveness and readiness probes:

yaml
1livenessProbe:
2 exec:
3 command:
4 - /bin/sh
5 - -c
6 - mysql -h localhost -u root -p${MYSQL_ROOT_PASSWORD} -e "SELECT 1" || exit 1
7 initialDelaySeconds: 30
8 periodSeconds: 10
9
10readinessProbe:
11 exec:
12 command:
13 - /bin/sh
14 - -c
15 - mysql -h localhost -u root -p${MYSQL_ROOT_PASSWORD} -e "SELECT 1" || exit 1
16 initialDelaySeconds: 5
17 periodSeconds: 5

Database Backup Strategy

bash
1# Backup WordPress PVC data
2kubectl exec deployment/mysql -- mysqldump -u root -p${MYSQL_ROOT_PASSWORD} --all-databases > backup.sql
3
4# Backup using kubectl cp
5kubectl cp mysql-<pod-id>:/var/lib/mysql ./mysql-backup
6
7# Restore from backup
8kubectl exec -i deployment/mysql -- mysql -u root -p${MYSQL_ROOT_PASSWORD} < backup.sql

Log Analysis

bash
1# View MySQL initialization logs
2kubectl logs mysql-<pod-id> | grep "ERROR"
3
4# Monitor WordPress errors
5kubectl logs wordpress-<pod-id> | grep "WordPress"
6
7# Real-time log streaming
8kubectl logs -f deployment/wordpress --all-containers=true

Common Issues and Solutions

Issue: WordPress Cannot Connect to MySQL

Symptoms: Error establishing database connection

Diagnosis:

bash
1# Check MySQL pod logs
2kubectl logs deployment/mysql
3
4# Verify MySQL service exists
5kubectl get svc mysql
6
7# Test DNS resolution from WordPress
8kubectl exec deployment/wordpress -- nslookup mysql

Solution:

  • Verify WORDPRESS_DB_HOST matches MySQL Service name exactly
  • Check MySQL pod is running: kubectl get pods | grep mysql
  • Verify credentials match between deployments

Issue: PVC Not Binding

Symptoms: Pending status in kubectl get pvc

Diagnosis:

bash
1kubectl describe pvc mysql-pvc | grep -A 20 "Events"

Causes and Solutions:

  • No available PV: Create PersistentVolume or use StorageClass for dynamic provisioning
  • AccessMode mismatch: ReadWriteOnce not available (check other bound PVCs)
  • Storage class not found: Verify storageClassName exists

Issue: High MySQL Memory Usage

Symptoms: Pod OOM killed or slow queries

Solutions:

sql
1-- Analyze buffer pool efficiency
2SHOW VARIABLES LIKE 'innodb_buffer_pool_%';
3
4-- Check current connections
5SHOW PROCESSLIST;
6
7-- Optimize configuration for K3s constraints
8SET GLOBAL innodb_buffer_pool_size = 256M;
9SET GLOBAL max_connections = 100;

Advanced Topics

Persistent Volume Reclaim Policies

Default policy: Delete - PV deleted when PVC is deleted

Alternative - Retain:

yaml
1spec:
2 persistentVolumeReclaimPolicy: Retain

Prevents accidental data loss; manual cleanup required.

Volume Snapshots for Disaster Recovery

yaml
1apiVersion: snapshot.storage.k8s.io/v1
2kind: VolumeSnapshot
3metadata:
4 name: mysql-snapshot
5spec:
6 volumeSnapshotClassName: csi-hostpath-snapclass
7 source:
8 persistentVolumeClaimName: mysql-pvc

StatefulSet Alternative (Production)

For better MySQL deployments, use StatefulSet instead of Deployment:

yaml
1apiVersion: apps/v1
2kind: StatefulSet
3metadata:
4 name: mysql
5spec:
6 serviceName: mysql
7 replicas: 1
8 selector:
9 matchLabels:
10 app: mysql
11 template:
12 # Same as Deployment template above
13 volumeClaimTemplates:
14 - metadata:
15 name: mysql-pvc
16 spec:
17 accessModes: ["ReadWriteOnce"]
18 resources:
19 requests:
20 storage: 10Gi

StatefulSet benefits:

  • Stable DNS names: mysql-0.mysql.default.svc.cluster.local
  • Ordered scaling and termination
  • Automatic PVC creation and binding per replica