Docker-izing WordPress for Kubernetes

WordPress is amazingly popular considering how antiquated the file structure and code appears to be. Even still, it is the easiest CMS that I have used and the community has created plugins to make the copy-folder-for-a-new-theme/plugin at least tolerable. A challenge comes when one wants to use the 1990’s method of serving web applications in a more modern way (such as running inside a container on top of Kubernetes). Containers are meant to be immutable and treated as read-only. (No change to files in the container after they are built.) Containers are supposed to be a point-in-time release of software. As such, I can roll-back to a specific container version and have that specific code running. This causes a problem when one wants to use a file-dependent application such as WordPress. The best I could come up with for running WordPress in a container is a forward-only method of deploying code (basically, giving up the ability to use a previous version of code.) Thereย isย a way to keep that functionality, but it would mean storing everything (including uploads) inside an ever-growing container or using a central object store such as S3 for uploads. It would also require a re-build of the container every time a plugin is updated – which would presumably be every hour. My deployments of WordPress are so little that I can hardly justify using S3 for uploads, keeping the plugins in sync, and going backwards in time. When deploying to Kubernetes, one can scale the replicas to N copies. Keeping plugins, themes, and updates the same across all replicas will require a READ WRITE MANY (rwx) volume to be shared. This could be a GlusterFS volume or NFS, but it cannot be a AWS EBS volume or any other single-use block storage. When looking at the available WordPress images, there are three that seem interesting. With the official image, I like that I can use php-fpm and alpine. The next top two implementations of WordPress have very bloated docker files. I have come to the conclusion that my WordPress container will have to be built from scratch.
The Dockerfile is very similar to the official WordPress container. It uses php:7.2-fpm-alpine as the base image, adds in nginx, and inserts a generic wp-config.php file.
The folder structure for the container is as follows:
WordPress Container Folder
โ”œโ”€โ”€ docker-entrypoint.sh
โ”œโ”€โ”€ Dockerfile
โ”œโ”€โ”€ html
โ”‚ย ย  โ””โ”€โ”€ ... Contents of wordpress-X.Y.Z.zip
โ”œโ”€โ”€ nginx.conf
โ””โ”€โ”€ wp-config.php
It can be built by running a command similar to docker build -t andrewwippler/wordpress:latest .
nginx.conf is a very basic configuration file with gzip and cache headers. The real neat things come in the docker-entrypoint.sh file.
I borrowed the database creation script; however, since PHP was already installed in the container, I ran a few more checks in PHP rather than bash. For instance, the container places the local code in /var/www/html-original and rsyncs it to /var/www/html where the webserver sees it, but it only does this if the code in html-original is newer than html. This allows an operator to mount a storage volume at /var/www/html which can be shared across Kubernetes Deployment replicas. The code for this is:
// see if we need to copy files over
include '/var/www/html-original/wp-includes/version.php';
$dockerWPversion = $wp_version;

if (file_exists('/var/www/html/wp-includes/version.php')) {
    include '/var/www/html/wp-includes/version.php';
    $installedWPversion = $wp_version;
} else {
    $installedWPversion = '0.0.0';
}

fwrite($stderr, "dockerWPversion: $dockerWPversion - installedWPversion: $installedWPversion\n");
if(version_compare($dockerWPversion, $installedWPversion, '>')) {
    fwrite($stderr, "Installing wordpress files\n");
    exec('rsync -au /var/www/html-original/ /var/www/html');
}
I have also included a theme-only check that will update the theme if it has changed. This is necessary to update the theme files when the version of WordPress has not changed.
if (filemtime('/var/www/html-original/wp-content/themes') > filemtime('/var/www/html/wp-content/themes')) {
    fwrite($stderr, "Updating theme files\n");
    exec('rsync -au --delete-after /var/www/html-original/wp-content/themes/ /var/www/html/wp-content/themes');
}
All files I have referenced in this article are located in a gist. In addition to those files a local docker-compose.yml file might be helpful for your local development:
version: '2'
services:
  db:
    image: mariadb:10
    volumes:
      - ./tmp/db:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=secretPASS

  wordpress:
    build: wordpress
    volumes:
      - ./html:/var/www/html
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    links:
      - db
    environment:
      - WORDPRESS_DB_NAME=wordpress
      - WORDPRESS_DB_HOST=db
      - WORDPRESS_DB_USER=root
      - WORDPRESS_DB_PASSWORD=secretPASS
    ports:
      - 8080:80