Hosting WordPress on Kubernetes

This is the sixth post in a series on Modernizing my Personal Web Projects. In this post I’ll describe my experience of hosting WordPress on Kubernetes for my personal sites.

WordPress is a popular free and open-source content management system. It’s often used for blogs and e-commerce sites for its ease of use and vast plugin library. I moved to it myself for my personal blog some time ago because it takes care of all the backend parts that power the site – so I can focus on writing content.

Prerequisites for Hosting WordPress on Kubernetes

One of WordPress’s most powerful features is the ability to self-update and customize the source code from within the site itself. For this to happen it requires persistent disk storage – something which doesn’t typically mix well with cloud services. Still, with it being so popular, there is plenty of support for running it on Kubernetes. For this tutorial there are a few prerequisites:

As usual I’ll be using Cloudflare for DNS and proxying.

Setting up the Database

The first thing to do is create the database that’ll be used for WordPress. If you don’t already have a managed database on DigitalOcean, you can create one with:

doctl databases create my-test-db --engine mysql --region sgp1

Next, create the WordPress database and user. To find the database ID use doctl databases list. Note the use of mysql_native_password – not all clients support the default password encryption yet, so this will be better for compatibility.

doctl databases db create (id of db) wordpress
doctl databases user create (id of db) wordpress --mysql-auth-plugin mysql_native_password

Then grant the user write access to the database by connecting to the database (see the ‘connection details’ in the DigitalOcean UI for instructions) and executing this SQL:

GRANT ALL PRIVILEGES ON wordpress.* to wordpress;

Migrating an Existing WordPress Database

If you have an existing WordPress installation (like me), you probably want to migrate its data to the new database. The standard MySQL tools can be used to do this:

# On the old database
mysqldump -u dbuser -p -h dbhost -P dbport somedb > somedb.sql
# On the new database
mysql -u dbuser -p -h dbhost -P dbhost somedb < somedb.sql

Deploying the Helm Chart

Now it’s time to install WordPress on the cluster. I used the excellent chart from Bitnami – there are an overwhelming number of options available, though.

First, create a YAML file to override the values, named for example my_wordpress.yaml:

## Database Settings
  port: 25060
  user: wordpress
  password: (the password)
  database: wordpress
## Use ClusterIP to use nginx as ingress
  type: ClusterIP
  enabled: true
  tls: true
  ingressClassName: nginx
## Add cert-manager annotations
  certManager: true
## Reduce resource requests
    memory: 256Mi
    cpu: 300m
## Disabling MariaDB
  enabled: false
## Reduce default persistent volume size
  size: 2Gi

Most of the overrides are self-explanatory and can be customized. The critical setting is externalDatabase and mariadb.enabled = false, to make WordPress use an external database. If you didn’t migrate an existing database, the default settings will be used and you will be presented with the setup wizard when first connecting to the site.

Install the chart with:

helm install my-wordpress bitnami/wordpress -f my_wordpress.yaml

Then navigate to your site (after setting up the DNS) to complete the installation.

Using DigitalOcean Spaces to Store Uploaded WordPress Files

For performance and cost reasons it’s a good idea to leverage DigitalOcean spaces to store uploaded WordPress media. This is more efficient than using a local block storage volume and allows you to serve files directly from the DigitalOcean CDN, taking load off the server.

There are expensive WordPress plugins that claim to do this, however there is an excellent free plugin I found: S3 Uploads by Human Made. There’s no admin UI but if you’re comfortable with the command-line that’s not an issue.

Installing the S3 Uploads Plugin

To install it:

  1. Download the from the releases page
  2. Create a new directory called s3-uploads in wp-content/plugins
  3. Upload the .zip into wp-content/plugins/s3-uploads using a file manager plugin
  4. Extract the .zip file

After installing it, log on to the WordPress pod to configure the plugin:

kubectl exec -it wp-pod-id bash
wp config set S3_UPLOADS_BUCKET your-bucket-name
wp config set S3_UPLOADS_REGION us-east-1 # note this isn't used
wp config set S3_UPLOADS_KEY your-spaces-key
wp config set S3_UPLOADS_SECRET your-spaces-secret
wp config set S3_UPLOADS_BUCKET_URL

Then apply a fix to the plugin by editing s3-uploads.php in the plugin editor in WordPress (otherwise the next command will fail):

if ( ! class_exists( '\\Aws\\S3\\S3Client' ) ) {
        // Require AWS Autoloader file.
       require_once dirname( __FILE__ ) . '/vendor/autoload.php';

add_filter( 's3_uploads_s3_client_params', function ( $params ) {
	$params['endpoint'] = '';
	$params['version'] = 'latest';
	return $params;
} );

Activate the plugin with

wp plugin activate s3-uploads
wp s3-uploads verify

Then copy any existing media files

wp s3-uploads upload-directory /bitnami/wordpress/wp-content/uploads uploads

Using Private Uploads

If your bucket is not open to the public, you can secure the files using S3 presigned URLs. To do so, edit s3-uploads.php from the WordPress plugin editor and add:

add_filter( 's3_uploads_is_attachment_private', '__return_true' );

Also, be sure to set the config setting so that newly uploaded files are marked private as well.

wp config set S3_UPLOADS_OBJECT_ACL private

Potential Issues

There were a few other issues I came across, which may or may not apply to you depending on your site’s settings.

Copying Files into WordPress

If you have existing files to migrate (plugins etc.), copying them is a little tricky. I ended up using the Advanced File Manager plugin to copy over the files, which can be zipped and extracted on the server side. However, if you do this you’re likely to hit the next issue.

Max Upload Size / 413 Payload Too Large

If you need to upload large files, you might run into the default limit on the NGINX ingress. This can easily be worked around by adding an annotation on the ingress: 256m

ABSPATH not working

I ran into an issue with the bitnami container due to using a symlink for wp-content. This changes the path used in __FILE__ / ABSPATH since they resolve the symlink, meaning that plugins and themes that rely on this method to find the WordPress root don’t work properly.

The fix I found was to change the use of __FILE__ where the error was occuring to use $_SERVER['SCRIPT_FILENAME'] instead. If you haven’t run into this issue, then you don’t need to worry.

Cloudflare Redirect Loop

When Cloudflare’s Flexible SSL mode is enabled, you might run into redirect loops when requesting certain files. I’m not sure of the root cause of this, but there are several plugins that fix it automatically. Cloudflare’s official plugin is one of them – just install it and you’re good to go. However, if you’ve followed the above steps then you can just use Cloudflare’s Full SSL mode instead.

Fixing Site Health Warning about WP_AUTO_UPDATE_CORE

If you see a warning on the dashboard like

The WP_AUTO_UPDATE_CORE constant is defined and enabled.

This can be fixed by setting the config value from within the WordPress pod, for example:

kubectl exec -it wordpress-pod-id wp config set WP_AUTO_UPDATE_CORE minor

Wrapping Up

Hosting WordPress on Kubernetes takes a bit of initial setup and can be less convenient to work with than a traditional installation. However, the benefits of auto-failover and portability are worth the effort. Furthermore, you’ll be able to use this pattern for all your WordPress sites, keeping things consistent and easy to manage.

Leave a comment

Your email address will not be published. Required fields are marked *