[Part 1/2] - WordPress and nginx with Docker Compose


PHP developers tend to get a slating from developers who code in other languages. This sucks. However, I've found PHP developers themselves tend to be pretty hard on some of their own: WordPress developers.

Everyone's entitled to their own opinion.

My opinion is that WordPress, as a platform, is without parallel.

Sure, if you dive below the surface and look at the code (and I'm predominantly talking about many of the third party plugins and themes here) you may be in for a shock. But the utility that WordPress provides would take hundreds or even thousands of hours of your own time to replicate.

But we aren't here to discuss the positives and negatives of WordPress code here. No.

In this video we are going to cover one way of getting a WordPress site up and running in Docker.

This is very much an opinionated way, and isn't likely 'best practice', nor truly adhering to the spirit of containerisation. But stick with me, as this as ever is focused more on how I use WordPress with Docker out there in the real world, rather than aiming for Computer Science perfection.

WordPress in Docker - My Approach

As mentioned, this is an opinionated approach to using WordPress inside a Docker container.

My opinion is that WordPress isn't a great candidate for typical containerisation.

Anyone who uses WordPress will have likely noticed that WordPress updates are frequent. This is good, we like security patches, bug fixes, and new features.

However, my typical WordPress installations also include a bunch of plugins, and maybe a custom theme, or even a theme provided by WordPress themselves.

All of these things update, and often, more than WordPress itself:

typical WordPress pending updates

With the classic approach to Docker, this presents a problem:

The Docker Image should be "done". All patches should be included, and ideally if there is a new patch, we update the base image file contents, and then build a new image and deploy that.

Practically, for me at least, this is unfeasible.

Even more so, if offering WordPress to clients, telling them that they cannot patch, or add plugins, or themes, without your intervention is a total non-starter.

Therefore, I prefer to mount all the WordPress data from a bind-mount on the host.

Yes, this has its limitations.

However, for my needs, this approach has proven to be an acceptable compromise.

Your opinion may differ, and that's fine. Feel free to alter, adapt, or completely disregard.

WordPress Docker Compose

To make our lives as easy as possible, we are going to use Docker Compose to manage our WordPress stack.

We will need three services:

  • MySQL (for the DB)
  • nginx (or Apache, if you prefer)
  • PHP (for WordPress)

We will need to have a copy of all the core WordPress files locally available. If you do not have these, please head over to the WordPress Download page and grab a copy of the latest zip file.

For the purposes of this video we will be using WordPress 4.8.2, but the process should be the same for any version of WordPress 4.x.x.

Once you have downloaded the files, unzip (or untar) into a new directory. I will be working in:

/home/chris/Development/docker-wordpress-example.dev

I like to put all my WordPress files into a subdirectory of this root directory. This means my configuration will live inside:

/home/chris/Development/docker-wordpress-example.dev

And my WordPress stuff will live in:

/home/chris/Development/docker-wordpress-example.dev/wordpress

Feel free to do whatever suits you.

The last setup step I take is to copy the wordpress/wp-config-sample.php into my project root directory:

cp wordpress/wp-config-sample.php ./wp-config.php`

This makes it much harder for me to accidentally overwrite the wp-config.php file if making wholesale changes to the wordpress directory.

We're now good to get started.

Using Environment Variables with WordPress and Docker Compose (Optional)

For the purposes of demonstration, we will make use of environment variables. As ever, please take heed of the same warning I always give:

Don't use environment variables in Production, unless you know what you are doing and understand the risks. Anything you set as an environment variable (e.g. passwords, secrets) will be available system-wide in plain text. Consider using hardcoded values, or more preferably, proper secrets in production.

However, for our purposes, we will be working in development, so environment variables can be beneficial here.

This said, please feel free to disregard this next step entirely, as you do not need to do this, especially if you only need one WordPress website, or you have no need for a staging, or test environment.

We will need to create a new file called .env.

touch .env

Into the .env file, please add:

AUTH_KEY=MY_AUTH_KEY
AUTH_SALT=MY_AUTH_SALT
MYSQL_HOST=db
MYSQL_DATABASE=wpdb_dev
MYSQL_USER=dbuser
MYSQL_PASSWORD=dbpassword
MYSQL_ROOT_PASSWORD=rootdbpassword
LOGGED_IN_KEY=MY_LOGGED_IN_KEY
LOGGED_IN_SALT=MY_LOGGED_IN_SALT
NONCE_KEY=MY_NONCE_KEY
SECURE_AUTH_KEY=MY_SECURE_AUTH_KEY
SECURE_AUTH_SALT=MY_SECURE_AUTH_SALT
WP_DEBUG=false

If you are familiar with WordPress you will likely recognise these key names as being those found in wp-config.php.

If you prefer, you can swap these values out for some generated ones from here.

As we are making use of environment variables we now need to update wp-config.php to take the values set in our system's environment variables, rather than the hardcoded values we usually provide:

<?php
/**
 * The base configuration for WordPress
 *
 * The wp-config.php creation script uses this file during the
 * installation. You don't have to use the web site, you can
 * copy this file to "wp-config.php" and fill in the values.
 *
 * This file contains the following configurations:
 *
 * * MySQL settings
 * * Secret keys
 * * Database table prefix
 * * ABSPATH
 *
 * @link https://codex.wordpress.org/Editing_wp-config.php
 *
 * @package WordPress
 */

// ** MySQL settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define('DB_NAME', getenv('MYSQL_DATABASE'));

/** MySQL database username */
define('DB_USER', getenv('MYSQL_USER'));

/** MySQL database password */
define('DB_PASSWORD', getenv('MYSQL_PASSWORD'));

/** MySQL hostname */
define('DB_HOST', getenv('MYSQL_HOST'));

/** Database Charset to use in creating database tables. */
define('DB_CHARSET', 'utf8');

/** The Database Collate type. Don't change this if in doubt. */
define('DB_COLLATE', '');

/**#@+
 * Authentication Unique Keys and Salts.
 *
 * Change these to different unique phrases!
 * You can generate these using the {@link https://api.wordpress.org/secret-key/1.1/salt/ WordPress.org secret-key service}
 * You can change these at any point in time to invalidate all existing cookies. This will force all users to have to log in again.
 *
 * @since 2.6.0
 */
$AUTH_KEY = getenv('AUTH_KEY');
$SECURE_AUTH_KEY = getenv('SECURE_AUTH_KEY');
$LOGGED_IN_KEY = getenv('LOGGED_IN_KEY');
$NONCE_KEY = getenv('NONCE_KEY');
$AUTH_SALT = getenv('AUTH_SALT');
$SECURE_AUTH_SALT = getenv('SECURE_AUTH_SALT');
$LOGGED_IN_SALT = getenv('LOGGED_IN_SALT');
$NONCE_SALT = getenv('NONCE_SALT');
define('AUTH_KEY',$AUTH_KEY);
define('SECURE_AUTH_KEY',$SECURE_AUTH_KEY);
define('LOGGED_IN_KEY',$LOGGED_IN_KEY);
define('NONCE_KEY',$NONCE_KEY);
define('AUTH_SALT',$AUTH_SALT);
define('SECURE_AUTH_SALT',$SECURE_AUTH_SALT);
define('LOGGED_IN_SALT',$LOGGED_IN_SALT);
define('NONCE_SALT',$NONCE_SALT);

/**#@-*/

/**
 * WordPress Database Table prefix.
 *
 * You can have multiple installations in one database if you give each
 * a unique prefix. Only numbers, letters, and underscores please!
 */
$table_prefix  = 'wp_';

/**
 * For developers: WordPress debugging mode.
 *
 * Change this to true to enable the display of notices during development.
 * It is strongly recommended that plugin and theme developers use WP_DEBUG
 * in their development environments.
 *
 * For information on other constants that can be used for debugging,
 * visit the Codex.
 *
 * @link https://codex.wordpress.org/Debugging_in_WordPress
 */
define('WP_DEBUG', getenv('WP_DEBUG'));

define('FORCE_SSL_ADMIN', false);
// in some setups HTTP_X_FORWARDED_PROTO might contain 
// a comma-separated list e.g. http,https
// so check for https existence
if (strpos($_SERVER['HTTP_X_FORWARDED_PROTO'], 'https') !== false)
       $_SERVER['HTTPS']='on';

/* That's all, stop editing! Happy blogging. */

/** Absolute path to the WordPress directory. */
if ( !defined('ABSPATH') )
    define('ABSPATH', dirname(__FILE__) . '/');

/** Sets up WordPress vars and included files. */
require_once(ABSPATH . 'wp-settings.php');

Essentially it's the exact same wp-config.php we all know and love, except we have use the getenv function grab the relevant environment variable from our config, rather than having hardcoded each value.

As mentioned, this section is entirely optional. You can use the 'classic' approach to wp-config.php if you prefer.

WordPress Docker File

We will need a docker container running PHP in order to serve our WordPress files.

Good news: We will make use of the PHP base image we created earlier in this series. It's lovely when we can re-use previous effort.

The Dockerfile is easy enough:

FROM codereviewvideos/php-7

ARG WORK_DIR
WORKDIR $WORK_DIR

RUN chown -R www-data:www-data $WORK_DIR

USER www-data

There's nothing we haven't seen here before.

We use the ARG instruction to set a docker build time argument that we can pass in when building the Docker image.

This ARG value will be used to set the WORKDIR / working directory inside the resulting running Docker container based off this image.

This means we can re-use this same Dockerfile as a starting point for any WordPress based project we might need.

RUN chown -R www-data:www-data $WORK_DIR - gotta get them permissions sorted :) In this case we want to make sure that the user and group are set to www-data.

Finally we tell Docker that when a container is run from this image, we want to become the USER www-data. This user has all the right permissions to our files at this point.

We now need to build this Docker Image. Remembering all these commands is not my forte, so I use a Makefile.

touch Makefile

And I would add the contents:

docker_build:
    @docker build \
        --build-arg WORK_DIR=/var/www/docker-wordpress-example.dev/ \
        -t codereviewvideos/php7-wordpress .

docker_push:
    @docker push codereviewvideos/php7-wordpress

bp: docker_build docker_push

Remember, being that this is a Makefile, use tabs not spaces.

As you can see, the --build-arg WORK_DIR=... sets the working directory for this project. It's hardcoded in the Makefile so I don't need to remember it in the future. If wanting to create another WordPress site for a different project, I simply need to copy / paste the .env, wp-config.php, Makefile, and Dockerfile, and change a few values and off I go.

In this example I'm pushing the resulting WordPress-friendly Docker image up to Docker Hub.

Code For This Course

Get the code for this course.

Episodes