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:

  1. You’ve installed cert-manager.
  2. 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.