WordPress on Kubernetes

The Definitive Guide to WordPress on k8s

Your First WordPress Deployment

We prioritize learning in this tutorial, rather than getting a WordPress deployment up and running as quickly as possible, so we’ll take things slow. We’ll start off with the simplest possible deployment, and eventually grow it into a fully fledged multi-node, highly available and scalable WordPress setup.

Note: all code samples from this section are available on GitHub.

Our first deployment will simply run the official WordPress container image from DockerHub. We will then expose the HTTP port of this container using a Kubernetes Service, making it available outside the cluster. We’ll confirm we can access the WordPress readme.html file. Finally, we’ll add a second container to our existing Pod for our database service, and connect the two together.

Running a WordPress container

The official WordPress images on DockerHub come in varoius flavors, but we’ll stick to the simplest one for our current use case – the Apache version. This container will run the Apache web server, capable of executing PHP code. It will also download a fresh copy of WordPress core files ready to be installed.

You’ll typically use Deployments, StatefulSets and other Kubernetes abstractions to create Pods, but for simplicity here we’ll just create a Pod directly. In an empty folder, create our first pod.yml manifest file with the following contents:

apiVersion: v1
kind: Pod
metadata:
  name: wordpress
  labels:
    app: wordpress
spec:
  containers:
  - name: wordpress
    image: wordpress:6.5-apache
    ports:
    - containerPort: 80

Let’s break it down. The first couple lines specify what kind of object you’ll want to create in Kubernetes.

The metadata section tells Kubernetes that the name of our pod is going to be wordpress (we’ll be able to address the pod by name later), and will also attach one label to our pod, with the key app and the value wordpress. This label is important and will allow our service to correctly identify which pod to route traffic to.

Next we have the spec section, with the specification for our Pod. Here we define a single container named wordpress with the WordPress image, and a single containerPort for our HTTP port 80.

We can go ahead and create this pod by applying the pod.yml manifest with kubectl:

$ kubectl apply -f pod.yml

Let’s now look at the pods in our cluster:

$ kubectl get pods
NAME        READY   STATUS    RESTARTS   AGE
wordpress   1/1     Running   0          6s

Note that the status might be stuck at ContainerCreating for a while as Kubernetes will need some time to pull the image from DockerHub.

After the container is Running, we can already access it from within the cluster, using curl for example. We can find out the IP address of the pod with kubectl using the -o wide flag:

$ kubectl get pods -o wide
NAME        READY   STATUS    RESTARTS   AGE     IP            NODE   NOMINATED NODE   READINESS GATES
wordpress   1/1     Running   0          4m25s   10.1.134.84   k1     <none>           <none>

In my example above, the pod is running on the k1 node and is using the 10.1.134.84 IP address in the cluster. Let’s SSH into any of the Kubernetes nodes in the cluster and request some data from that IP:

$ ssh k1
$ curl -v -o /dev/null 10.1.134.84/readme.html
* Connected to 10.1.134.84 (10.1.134.84) port 80
> GET /readme.html HTTP/1.1
< HTTP/1.1 200 OK
< Server: Apache/2.4.57 (Debian)

I’ve omitted some of the output for brevity, but we can see here that the IP address accepts our connection on port 80, and responds to our HTTP request with the contents of the readme.html file.

This is good progress, but we’ll want that pod accessible from outside of our Kubernetes cluster. We’ll also want it to have a more predictable IP address since Pod addresses are throw-away in Kubernetes and will change during restarts, crashes, etc. This is where a Kubernetes Service component will help us out.

Creating a Service

There are a few different types of services for different use cases and we’ll work with a few of them throughout this tutorial. We’ll use a simple NodePort service here, which will expose our Apache port 80 over a specific port across all nodes in our Kubernetes cluster.

Let’s create our service.yml manifest in the same directory with the following contents:

apiVersion: v1
kind: Service
metadata:
  name: wordpress
spec:
  type: NodePort
  ports:
  - port: 80
    nodePort: 30007
  selector:
    app: wordpress

The first few lines, as you may have already guessed, define the Kubernetes object type and some metadata about the service. The spec defines the service type, the port mapping and our selector.

The service type is set to NodePort, which allocates a port (that we specify later) on every Node in the Kubernetes cluster. Every node in the cluster will be able to receive traffic on this port, and will handle the forwarding to our pods accordingly. This type of service is not recommended production deployments and we’ll cover some better options in later sections, but for development and testing purposes NodePort services are great.

The ports mapping specifies which container port we’d like to map to (the HTTP port 80 in our case), and the nodePort we’d like to use. The node port has to be unique for the cluster and in a specific range (30000-32767 by default). If not specified, Kubernetes will allocate a random port from that range automatically on service creation. We use the port 30007 in our example.

Finally, the selector is where we define which Pods should this service direct traffic to. We do this using labels, and in the example above we tell the Kubernetes service to find all pods with a label with the key app and the value wordpress.

Let’s now create this service with kubectl:

$ kubectl apply -f service.yml

And look at our services list:

$ kubectl get services
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
kubernetes   ClusterIP   10.152.183.1    <none>        443/TCP        6d16h
wordpress    NodePort    10.152.183.95   <none>        80:30007/TCP   5m36s

You should see the new wordpress service with the NodePort type, and port 30007 mapped to port 80. Don’t worry about the Cluster or External IPs for now, we’ll touch on these in later sections. After the service has been created, we should be able to access our WordPress installation through the exposed port on any of the Kubernetes nodes from outside the cluster:

$ curl -v -o /dev/null k0:30007/readme.html
* Connected to 192.168.1.120 (192.168.1.120) port 30007
> GET /readme.html HTTP/1.1
< HTTP/1.1 200 OK
< Server: Apache/2.4.57 (Debian)

As you can see, we’re no longer requesting the pod IP address (which is still 10.1.134.84 and not accessible outside of the Kubernetes cluster), but instead we’re using one of the Kubernetes node’s IP address. You can try this in your browser too and you should be able to see the contents of the WordPress readme.html file.

However, if you try to load the homepage, you’ll see an “Error establising a database connection” message, so we’ll tackle that next.

Adding a MySQL/MariaDB database

WordPress requires a MySQL-compatible database to run. In this tutorial we’ll mostly be using MariaDB, but MySQL, Percona and others will usually work out of the box as well. For simplicity, we’ll add a MariaDB container to the same Pod that’s running our WordPress container.

Let’s edit our pod.yml file and add a new entry to the containers section on the same level as our wordpress container:

  - name: mariadb
    image: mariadb:10.11
    ports:
    - containerPort: 3306
    env:
    - name: MARIADB_DATABASE
      value: wordpress
    - name: MARIADB_USER
      value: wordpress
    - name: MARIADB_PASSWORD
      value: secret
    - name: MARIADB_RANDOM_ROOT_PASSWORD
      value: verysecret

In addition to the container name, image and port definitions, this container will also have some environment variables defined in the env section. They’re pretty self-explanatory, and are required for the MariaDB container to initialize a database for us using these credentials. We’ll hard-code our credentials here for now, and later move them to a proper Kubernetes Secret and ConfigMap for better security and ease of management.

Now that we’ve defined a MariaDB container, let’s go back to our WordPress container and tell it where to find the database and which credentials to use. We can do this using environment variables as well, so our WordPress container definition will look as follows:

  - name: wordpress
    image: wordpress:6.4-apache
    ports:
    - containerPort: 80
    env:
    - name: WORDPRESS_DB_HOST
      value: 127.0.0.1
    - name: WORDPRESS_DB_USER
      value: wordpress
    - name: WORDPRESS_DB_NAME
      value: wordpress
    - name: WORDPRESS_DB_PASSWORD
      value: secret

Note that we’re using 127.0.0.1 as our WordPress database host, which tells the WordPress container to look for the database service in the same Kubernetes Pod, under the default MySQL/MariaDB port of 3306. You can normally use localhost in many cases for inter-pod communication, however in PHP’s MySQL driver, localhost has a special meaning which will cause it to connect via a Unix socket instead, which we do not have, so we need to explicitly use the localhost IP address to force a TCP connection.

Again, in future sections we’ll move the database credentials out into Kubernetes ConfigMaps and Secrets, but for simplicity these can temporarily live in plain text in our manifest files.

Before you can apply this new manifest file in your Kubernetes cluster, you’ll need to delete the old running WordPress pod:

$ kubectl delete pod wordpress
$ kubectl apply -f pod.yml
pod/wordpress created

Now, navigate your browser to any node IP address and the service NodePort you’ve define, for example http://10.0.10.120:30007 or http://k0:30007 in our case (the k1, and k2 hosts should also work), and you should be greeted with the famous WordPress installer. Pick a language, set your title and admin credentials, and log in to your brand new WordPress dashboard.

WordPress installer in Kubernetes

Congratulations! You’re now running WordPress in your Kubernetes cluster, but this is far from ideal.

This is just the beginning of your WordPress on Kubernetes journey. In the following sections you will learn how to add Volumes and Persistent Volume Claims in Kubernetes for data persistence, so that a Pod restart doesn’t destroy all your WordPress and MariaDB data. You’ll learn how to decouple the database container out into a separate pod and service for independent management. You’ll learn how to use Deployments and StatefulSets for container lifecycle management. You’ll learn how to transform WordPress into a stateless application for easier scaling and high availability. You will learn some better ways of getting traffic into your Kubernetes cluster with Load Balancers and Ingress controllers, and much much more.

Delete your pod and service (all data will be erased) with the following command:

$ kubectl delete -f pod.yml -f service.yml
pod "wordpress" deleted
service "wordpress" deleted

See you in the next section!