Docker WordPress in a subdirectory

Moving a standard WordPress installation to a different host is a minor pain – I only do this occasionally, so every time I need to consider the configuration of the original environment and how this translates to the new server. Nothing too challenging, but tedious and prone to error.

So I figured Docker containers are the way to go and, sure enough, Docker Hub has more than enough images for my needs. The only issue is that I don’t dedicate my server to WordPress – it’s in a ./wordpress subdirectory of the web root. Docker’s official WordPress image keeps reinstating the WordPress files if they’re not found in the web root.

TL;DR – create the directory wordpress in the container’s web root and add -w="/var/www/html/wordpress" to the docker run (or create) command. This sets the current working directory for docker-entrypoint.sh to work in, and it will install the wp-* and .htaccess files there.

The rest of this post documents my setup, more than anything it’s just a future reference for myself. I’ll start with articles that were references when I started setting this up.

The first is How to Install WordPress with Docker on Ubuntu which is a clearly written tutorial that goes further and uses Nginx as a reverse proxy to the container (I’ve chosen not to do for now). The second is Install fail2ban with Docker which describes what’s required to get fail2ban configured to read a container’s logs. Although I don’t document anything further on this, it’s really useful to inhibit brute-forcing of the server.

Let’s Encrypt Verification via DNS

For me, the least invasive way to verify domain ownership for  SSL/TLS was via DNS TXT records. This avoids the need to integrate with web servers and bind to/forward from a public port, however it does mean that I have to edit my DNS zone to add the required TXT records as instructed by certbot.

certbot certonly --manual --preferred-challenges=dns\
  -d example.com\
  -d www.example.com

At the end of this process, the certificates are placed in /etc/letsencrypt/live. Apache’s SSL virtual host can be configured with the following directives, after the WordPress container is created as detailed below.

SSLCertificateFile /etc/letsencrypt/live/example.com/cert.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem

The /etc/letsencrypt directory can be brought with the container to a new host and the certificates will be made visible to the WordPress container using a bind-mount, as detailed below. If you only want to transfer the above files in /etc/letsencrypt/live, then remember that they’re links into ../archive/, so they must be dereferenced if an archive tool is used (e.g. option --dereference in the  tar command).

Creating the containers

A container is created from the Docker Hub wordpress image, and also from the mariadb image. These run with bind-mounts to expose host directories inside the containers. The directory structure on the host has /opt/wordpress/html for the WordPress container, /opt/wordpress/database for the MariaDB container.

mkdir -p /opt/wordpress/databasemkdir -p /opt/wordpress/html/wordpress

I also mount /etc/letsencrypt/live as a read-only directory for Apache SSL/TLS. This directory must exist on the host, with files susa.net/cert.pem and susa.net/fullkey.pem (Apache needs these for default-ssl.conf).

The following commands assume that these directories have been created, and that valid certificates have been created (see above for notes if this hasn’t been done yet). First, we create the MariaDB container for our WordPress data.

docker run -e MYSQL_ROOT_PASSWORD=<mysqlrootpw>\
 -e MYSQL_USER=wpuser -e MYSQL_PASSWORD=<wpuserpw>\
 -e MYSQL_DATABASE=wordpress_db\
 -v /opt/wordpress/database:/var/lib/mysql\
 --name wp-mariadb -d mariadb

Next we create the WordPress container. This links to the wp-mariadb container we have just created to expose the host as mysql. It also exposes the wp-mariadb container’s environment to our wp-apache2 container, so we unnecessarily divulge, for example MYSQL_ROOT_PASSWORD, to a public Internet server. This is not ideal (e.g. wp-apache2 has no need to know MYSQL_ROOT_PASSWORD), and is probably why --link is being deprecated in favour of Docker networks.

docker run -w="/var/www/html/wordpress"\
 -e WORDPRESS_DB_USER=wpuser\
 -e WORDPRESS_DB_PASSWORD=<wpuserpw>\
 -e WORDPRESS_DB_NAME=wordpress_db\
 -p 80:80 -p 443:443\
 -v /opt/wordpress/html:/var/www/html\
 -v /etc/letsencrypt/live:/etc/letsencrypt/live:ro\
 --link wp-mariadb:mysql --name wp-apache2 -d wordpress

It should now be possible to access Apache on port 80 and port 443, and WordPress should be on the path /wordpress/.

Note the -d flag that detaches the process and returns control back to the calling shell. This is essential if the containers are to run in the background, and can be omitted to keep the process in the foreground, useful when you want logs to be reported to stdout.

Some commands used while setting up the wp-apache2 container.

Most of the commands below should really be brought into to a DockerFile configuration, but it’s convenient for my use case to simply build a baseline image that can ultimately be committed.

docker exec -it wp-apache2 bash  # This takes us to bash in the container
a2enmod ssl   # These commands are run on bash in the container
a2ensite default-ssl
apt-get install vim
vi /etc/apache2/sites-enabled/default-ssl.conf

For wp-mariadb, I often find it useful when testing to get WordPress to respond to a different siteurl, so I created a bash script update_wp_siteurl.sh in the root directory.

#!/bin/bash

if [ "${1}" == "" ]; then
  echo "Usage ${0} URL (such as http://www.susa.net/wordpress)"
  exit 0
fi
mysql -u${MYSQL_USER} -p${MYSQL_PASSWORD} ${MYSQL_DATABASE} <<EOSQL
  update wp_options set option_value = '${1}'
   where option_name in ('home', 'siteurl');
  select option_name, substr(option_value, 1, 60) as option_value
   from wp_options
   where option_name in ('home', 'siteurl');
EOSQL

Managing the images and migration

The following commands will commit images to the repository and save those images and gzipped tar files. I use the tag to denote the host on which the container’s image was created, in my case here I my host is kakapo.

docker commit wp-apache2 wordpress:kakapo
docker commit wp-mariadb mariadb:kakapo
docker image save wordpress:kakapo |gzip > wordpress-kakapo_image.tgz
docker image save mariadb:kakapo |gzip > mariadb-kakapo_image.tgz

The last two lines can be condensed into a single command for convenience.

docker image save\
  wordpress:kakapo\
  mariadb:kakapo | gzip > wordpress-mariadb-kakapo_images.tgz

The saved files can be copied to a remote host and loaded into the new host’s Docker repository.

kevin@kakapo:~$ scp wordpress-mariadb-kakapo_images.tgz newhost.example.com:

The the new host, load the images with

kevin@newhost:~$ zcat wordpress-mariadb-kakapo_images.tgz | docker image load

The newly loaded images can be used to create containers on the new host as described above, only using the images wordpress:kakapo and mariadb:kakapo instead of pulling the official images.

The bind-mounts

Remember that, before creating the containers on a new host, the /opt/wordpress/ and /etc/letsencrypt/ directories have to be transferred and accessible to docker in the location specified by the -v (or --mount) parameter. Depending on the environment, something like rsync or tar then scp should suffice.

On the new host, make sure that ./wp-content/* is writeable by the user and/or group www-data. I usually run Debian, so the host UID/GID for www-data is the same as in /etc/passwd in the WordPress container. Therefore it’s enough to simply chown -R www-data:www-data wp-content.

Generally, when using bind mounts, the permissions have to be considered from the container’s point of view. The directory can be seen both from the host, and from within the container environment. The UID/GID of the files will be interpreted according to the environment that’s reading the filesystem. If a file is writeable only by UID 1000 on the host (whoever that may be), then only processes running as UID 1000 in a container will be able to write the file.

Docker Volumes should really be used instead. They can be managed directly using Docker, so as long as I can also get access to the files from the host, then volumes would be a better way to go. Something to do in future.


Leave a Reply

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