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
1nano mysql.yaml1apiVersion: apps/v12kind: Deployment3metadata:4 name: mysql5 labels:6 app: mysql7spec:8 replicas: 19 selector:10 matchLabels:11 app: mysql12 template:13 metadata:14 labels:15 app: mysql16 spec:17 containers:18 - name: mysql19 image: mysql:820 env:21 - name: MYSQL_ROOT_PASSWORD22 value: rootpassword23 - name: MYSQL_DATABASE24 value: wordpress25 - name: MYSQL_USER26 value: wpuser27 - name: MYSQL_PASSWORD28 value: wppassword29 ports:30 - containerPort: 330631 volumeMounts:32 - name: mysql-pvc33 mountPath: /var/lib/mysql34 volumes:35 - name: mysql-pvc36 persistentVolumeClaim:37 claimName: mysql-pvc38---39apiVersion: v140kind: PersistentVolumeClaim41metadata:42 name: mysql-pvc43spec:44 accessModes:45 - ReadWriteOnce46 resources:47 requests:48 storage: 10Gi49---50apiVersion: v151kind: Service52metadata:53 name: mysql54spec:55 type: LoadBalancer56 ports:57 - port: 330658 targetPort: 330659 selector:60 app: mysqlCreate deployment WordPress
1nano wp.yaml1apiVersion: apps/v12kind: Deployment3metadata:4 name: wordpress5 labels:6 app: wordpress7spec:8 replicas: 19 selector:10 matchLabels:11 app: wordpress12 template:13 metadata:14 labels:15 app: wordpress16 spec:17 containers:18 - name: wordpress19 image: wordpress:latest20 ports:21 - containerPort: 8022 env:23 - name: WORDPRESS_DB_HOST24 value: mysql25 - name: WORDPRESS_DB_USER26 value: wpuser27 - name: WORDPRESS_DB_PASSWORD28 value: wppassword29 - name: WORDPRESS_DB_NAME30 value: wordpress31 volumeMounts:32 - name: wordpress-pvc33 mountPath: /var/www/html34 volumes:35 - name: wordpress-pvc36 persistentVolumeClaim:37 claimName: wordpress-pvc38---39apiVersion: v140kind: Service41metadata:42 name: wordpress43spec:44 type: LoadBalancer45 selector:46 app: wordpress47 ports:48 - port: 8049 targetPort: 80Create PVC for WordPress
1nano pvc.yaml1apiVersion: v12kind: PersistentVolumeClaim3metadata:4 name: wordpress-pvc5spec:6 accessModes:7 - ReadWriteOnce8 resources:9 requests:10 storage: 10GiDeploy all resources
1kubectl apply -f pvc.yaml2kubectl apply -f mysql.yaml3kubectl apply -f wp.yamlVerification and Troubleshooting
Verify Deployment Status
1# Check pod status and readiness2kubectl get pods -o wide3
4# Get detailed pod information5kubectl describe pod mysql-<pod-id>6kubectl describe pod wordpress-<pod-id>7
8# Monitor pod logs for startup errors9kubectl logs mysql-<pod-id>10kubectl logs wordpress-<pod-id> -fVerify PVC Binding
1# List all PersistentVolumeClaims2kubectl get pvc3
4# Inspect PVC details5kubectl describe pvc mysql-pvc6kubectl describe pvc wordpress-pvc7
8# Check PersistentVolume status9kubectl get pvCheck Service and LoadBalancer
1# List services and external IPs2kubectl get svc3
4# Watch service assignment of external IP5kubectl get svc -w6
7# Test MySQL connectivity from within cluster8kubectl 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 accessReadWriteMany: 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 initializationMYSQL_USER: Creates non-root user with permissions onMYSQL_DATABASEMYSQL_PASSWORD: Password for theMYSQL_USERaccount
Security Considerations:
- Root password stored in plain YAML (development only)
- For production, use Kubernetes Secrets:
1apiVersion: v12kind: Secret3metadata:4 name: mysql-credentials5type: Opaque6stringData:7 root-password: ${SECURE_ROOT_PASSWORD}8 user-password: ${SECURE_USER_PASSWORD}9---10# Reference in deployment:11- name: MYSQL_ROOT_PASSWORD12 valueFrom:13 secretKeyRef:14 name: mysql-credentials15 key: root-passwordMySQL 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):
1-- Check current settings2SHOW VARIABLES LIKE 'innodb_buffer_pool_size';3
4-- InnoDB uses ~75-80% of available memory for buffer pool5-- For K3s nodes with 2GB available: set to ~1.5GBWordPress Application Configuration
Initialization Flow
WordPress requires database connectivity during startup:
- Image Pull:
wordpress:latest(Apache + PHP + WordPress) - Entrypoint Script: Detects
wp-config.phpmissing, generates from environment - Database Connection: Attempts MySQL connection via
WORDPRESS_DB_HOST - WordPress Setup: Creates tables and initializes content
- Service Ready: Serves HTTP requests on port 80
Environment Variable Dependencies
1WORDPRESS_DB_HOST: mysql # Must match MySQL Service name (DNS resolvable)2WORDPRESS_DB_USER: wpuser # Must exist in MySQL with database permissions3WORDPRESS_DB_PASSWORD: wppassword # Credentials match MySQL setup4WORDPRESS_DB_NAME: wordpress # Must pre-exist in MySQLDNS 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:
1# Check LoadBalancer status2kubectl get svc wordpress -o jsonpath='{.status.loadBalancer.ingress[0].ip}'3
4# Access WordPress via external IP5curl http://<EXTERNAL_IP>Network Policies (Optional Enhancement)
1apiVersion: networking.k8s.io/v12kind: NetworkPolicy3metadata:4 name: wordpress-to-mysql5spec:6 podSelector:7 matchLabels:8 app: wordpress9 policyTypes:10 - Egress11 egress:12 - to:13 - podSelector:14 matchLabels:15 app: mysql16 ports:17 - protocol: TCP18 port: 330619 - to:20 - namespaceSelector: {}21 ports:22 - protocol: TCP23 port: 53 # DNS24 - protocol: UDP25 port: 53Performance Optimization
Resource Requests and Limits
Enhance deployments with resource specifications:
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,nodiratimecan improve performance
Monitor using:
1kubectl top pod <pod-name>2kubectl top nodeScaling 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:
1livenessProbe:2 exec:3 command:4 - /bin/sh5 - -c6 - mysql -h localhost -u root -p${MYSQL_ROOT_PASSWORD} -e "SELECT 1" || exit 17 initialDelaySeconds: 308 periodSeconds: 109
10readinessProbe:11 exec:12 command:13 - /bin/sh14 - -c15 - mysql -h localhost -u root -p${MYSQL_ROOT_PASSWORD} -e "SELECT 1" || exit 116 initialDelaySeconds: 517 periodSeconds: 5Database Backup Strategy
1# Backup WordPress PVC data2kubectl exec deployment/mysql -- mysqldump -u root -p${MYSQL_ROOT_PASSWORD} --all-databases > backup.sql3
4# Backup using kubectl cp5kubectl cp mysql-<pod-id>:/var/lib/mysql ./mysql-backup6
7# Restore from backup8kubectl exec -i deployment/mysql -- mysql -u root -p${MYSQL_ROOT_PASSWORD} < backup.sqlLog Analysis
1# View MySQL initialization logs2kubectl logs mysql-<pod-id> | grep "ERROR"3
4# Monitor WordPress errors5kubectl logs wordpress-<pod-id> | grep "WordPress"6
7# Real-time log streaming8kubectl logs -f deployment/wordpress --all-containers=trueCommon Issues and Solutions
Issue: WordPress Cannot Connect to MySQL
Symptoms: Error establishing database connection
Diagnosis:
1# Check MySQL pod logs2kubectl logs deployment/mysql3
4# Verify MySQL service exists5kubectl get svc mysql6
7# Test DNS resolution from WordPress8kubectl exec deployment/wordpress -- nslookup mysqlSolution:
- Verify
WORDPRESS_DB_HOSTmatches 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:
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:
ReadWriteOncenot available (check other bound PVCs) - Storage class not found: Verify
storageClassNameexists
Issue: High MySQL Memory Usage
Symptoms: Pod OOM killed or slow queries
Solutions:
1-- Analyze buffer pool efficiency2SHOW VARIABLES LIKE 'innodb_buffer_pool_%';3
4-- Check current connections5SHOW PROCESSLIST;6
7-- Optimize configuration for K3s constraints8SET 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:
1spec:2 persistentVolumeReclaimPolicy: RetainPrevents accidental data loss; manual cleanup required.
Volume Snapshots for Disaster Recovery
1apiVersion: snapshot.storage.k8s.io/v12kind: VolumeSnapshot3metadata:4 name: mysql-snapshot5spec:6 volumeSnapshotClassName: csi-hostpath-snapclass7 source:8 persistentVolumeClaimName: mysql-pvcStatefulSet Alternative (Production)
For better MySQL deployments, use StatefulSet instead of Deployment:
1apiVersion: apps/v12kind: StatefulSet3metadata:4 name: mysql5spec:6 serviceName: mysql7 replicas: 18 selector:9 matchLabels:10 app: mysql11 template:12 # Same as Deployment template above13 volumeClaimTemplates:14 - metadata:15 name: mysql-pvc16 spec:17 accessModes: ["ReadWriteOnce"]18 resources:19 requests:20 storage: 10GiStatefulSet benefits:
- Stable DNS names:
mysql-0.mysql.default.svc.cluster.local - Ordered scaling and termination
- Automatic PVC creation and binding per replica