Running WordPress in Docker
I’ve always enjoyed taking things apart to see how they tick, which is probably why I find myself running WordPress inside Docker on a K3s cluster these days. It isn’t the only way to host it, and it isn’t even the simplest way, but it gives you control, flexibility and repeatability that plain shared hosting never could. My setup lives on a Hetzner server using K3s and Rancher, but the whole approach should work just as well on any other Kubernetes environment.
WordPress and Kubernetes might sound like an odd couple, and maybe they are, but once everything is wired together you get a clean deployment pipeline and a site you can tear down and rebuild whenever you need. Let’s walk through the basics.
The Database: MySQL in a Container
WordPress needs a database. Nothing crazy, really. A standard MySQL or MariaDB instance will do you. In Kubernetes, that means creating a Deployment and a Service, but before that we need persistent storage. Databases without persistent storage are about as much use as a bag of monkeys. Not that I’d put monkeys in a bag. I love monkeys.
Persistent Volume and Claim for MySQL
Here’s a simple PV/PVC pair using Hetzner’s built-in storage class. If you’re on another platform, swap in the right storage class name and you’re good.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: hcloud-volumes
resources:
requests:
storage: 10Gi
MySQL Deployment
Nothing complicated. One container, a mounted volume and a few environment variables. Boom.
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql
spec:
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: root-password
- name: MYSQL_DATABASE
value: wordpress
volumeMounts:
- name: mysql-storage
mountPath: /var/lib/mysql
volumes:
- name: mysql-storage
persistentVolumeClaim:
claimName: mysql-pvc
You’ll also want a Service so WordPress can find it, which always helps:
apiVersion: v1
kind: Service
metadata:
name: mysql
spec:
ports:
- port: 3306
selector:
app: mysql
And that’s the database done. Not glamorous, but solid.
Persistent Storage for WordPress
WordPress stores uploads, themes and plugins on disk, so it also needs a persistent volume. Same idea as the database, just separate storage.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: wordpress-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: hcloud-volumes
resources:
requests:
storage: 10Gi
Running WordPress Itself
Once the storage and database are sorted, WordPress is fairly straightforward. The official Docker image works well, so we’ll just use that.
apiVersion: apps/v1
kind: Deployment
metadata:
name: wordpress
spec:
selector:
matchLabels:
app: wordpress
template:
metadata:
labels:
app: wordpress
spec:
containers:
- name: wordpress
image: wordpress:php8.2-apache
env:
- name: WORDPRESS_DB_HOST
value: mysql
- name: WORDPRESS_DB_USER
value: root
- name: WORDPRESS_DB_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: root-password
- name: WORDPRESS_DB_NAME
value: wordpress
volumeMounts:
- name: wp-content
mountPath: /var/www/html/wp-content
volumes:
- name: wp-content
persistentVolumeClaim:
claimName: wordpress-pvc
And a Service to expose it internally:
apiVersion: v1
kind: Service
metadata:
name: wordpress
spec:
ports:
- port: 80
selector:
app: wordpress
So, now, WordPress is running, the database is alive and you’ve got persistent disks under both. But nobody can see it yet. I learned early on that this is a fairly essential requirement of a website, so let’s sort that out.
Ingress and SSL
To expose your shiny new WordPress website, you need an Ingress controller. K3s ships with Traefik by default, so we’ll work with that. If you’re on another Kubernetes setup using Nginx or something else, the configuration will be similar.
Here’s a basic Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: wordpress-ingress
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
cert-manager.io/cluster-issuer: letsencrypt
spec:
tls:
- hosts:
- example.com
secretName: wordpress-tls
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: wordpress
port:
number: 80
This assumes two things:
- You’ve installed cert-manager.
- You’ve created a ClusterIssuer that points at Let’s Encrypt.
Once that’s in place, Traefik handles HTTPS termination, cert-manager handles the certificates, and your WordPress site becomes a fully fledged secure deployment instead of something only you can access on port 80, which is fairly inconvenient these days, as most browsers will put obstacles in the path of accessing not HTTPS sites.
Automation with GitHub Actions
This whole setup becomes much more fun when you automate builds and deployments. Docker images, updates, theme changes, plugin tweaks, all triggered by commits in your repo. GitHub Actions can build your image, push it to GHCR, update manifests and roll out changes automatically.
I won’t dig into the workflow file here (that’s a whole article on its own), but the gain is huge: deterministic deployments, versioned infrastructure and a workflow that feels closer to modern application development than traditional FTP-based WordPress hosting.
Final Thoughts
Running WordPress in Docker on a K8s/K3s cluster isn’t for everyone, but it gives you a level of control that’s hard to beat. A clean stack with isolated services, persistent volumes, automated deployments and proper monitoring all wired in. And whether you’re on Hetzner, AWS, a Raspberry Pi cluster or something in between, the steps stay roughly the same. It’s also pretty good for diving into basic DevOps type stuff, too.
So, there you go. WordPress, in Docker, running on Kubernetes. With some minor tweaks, all of this should be applicable in whatever environment you’re using, but if you wanted to give K3s and Rancher a try, like I did, you can check out the post I wrote about setting that up here.