Having recently been learning about Docker I decided that I should upgrade my blog from an old and clumsy webhost (TSOHost) to a DigitalOcean VM that I can manage myself. This was a great exercise in learning how to setup a real world scenario and there were several challenges to overcome. Ultimately, I came up with this docker-compose file.

version: '3'
services:
  nginx:
    image: "nginx"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/log/nginx:/var/log/nginx
      - /opt/nginx/nginx.conf:/etc/nginx/nginx.conf
      - /opt/nginx/sites-enabled:/etc/nginx/sites-enabled
      - /opt/nginx/ssl:/etc/nginx/ssl
    depends_on:
      - "wpcontainer"
    networks:
      - frontend
      - backend
  wpcontainer:
    image: "wordpress"
    networks:
      - frontend
      - backend
    volumes:
      - /opt/wordpress/html:/var/www/html
    depends_on:
      - "wpdatabase"
    environment:
      - WORDPRESS_DB_USER=dbuser
      - WORDPRESS_DB_PASSWORD=dbpassword
      - WORDPRESS_DB_NAME=dbname
      - WORDPRESS_DB_HOST=wpdatabase
  wpdatabase:
    image: "mariadb"
    volumes:
      - /opt/wordpress/database:/var/lib/mysql
    networks:
      - backend
    environment:
      - MYSQL_ROOT_PASSWORD=dbrootpass
      - MYSQL_USER=dbuser
      - MYSQL_PASSWORD=dbpassword
      - MYSQL_DATABASE=dbname

networks:
  frontend:
  backend:
   internal: true

This launches three containers in the order of database, wordpress, and finally nginx. This is the config for nginx.

server {
 listen 80;
 server_name mcgrane.uk mcgrane.co.uk www.mcgrane.uk www.mcgrane.co.uk;
 access_log /var/log/nginx/mcgrane-access.log;
 error_log /var/log/nginx/mcgrane-error.log;
 return 302 https://mcgrane.uk$request_uri;
}

server {
 listen 443 ssl;
 server_name mcgrane.uk www.mcgrane.uk;
 ssl_certificate /etc/nginx/ssl/mcgrane.pem;
 ssl_certificate_key /etc/nginx/ssl/mcgrane.key;

 access_log /var/log/nginx/mcgrane-ssl-access.log;
 error_log /var/log/nginx/mcgrane-ssl-error.log;
 location / {
  proxy_set_header Host $host;
  proxy_pass http://wpcontainer:80;
 }
}

server {
 listen 443 ssl;
 server_name mcgrane.co.uk www.mcgrane.co.uk;
 ssl_certificate /etc/nginx/ssl/mcgrane.pem;
 ssl_certificate_key /etc/nginx/ssl/mcgrane.key;
 return 302 https://mcgrane.uk$request_uri;
}

Problems

There were a few issues I came across while setting up this project.

WordPress URL

As part of the move I wanted to migrate from .co.uk to the .uk variant of my domain so had to edit the SQL dump. This was relatively easy when using Find & Replace in Notepad++

WordPress Broken!

Part of the reason I wanted to leave the former host was because their support staff had somehow broken WordPress and I didn’t know enough about the platform to fix it myself. I eventually discovered that the theme and plugins were all broken. The fix was to delete the wp-content/plugins directory and update the wp_options table to set option_value to a known good theme – I used tewntynineteen – where option_name was ‘template‘ or ‘stylesheet‘ (thanks!)

Everything is Missing

I’d forgotten to upload the content of wp-content/uploads which, as it turns out, is kind of important if you want to keep all of your images!

Force HTTPS / Fix Permalinks

At this point everything was happening over unencrypted HTTP so I figured out how to do the HTTPS nginx configuration then found that none of the images worked. This was due to to the images trying to load from an unencrypted source so I followed some simple advice to reset all permalinks by updating Settings->General WordPress Address and Site Address then going to Settings->Permalinks and changing Common Settings to Custom Structure then back to Post name and the links all seemed fine after that.

Docker Container Fails to Start

The first real issue I had was trying to make the containers start in the correct order and this is what lead me on to using docker-compose. It worked out very well and eventually this approach let me set up the deployment as a linux service (systemd file shown further down). Using a docker-compose file I can launch and tear down the containers very easily and, as a service, the containers can automatically start in the correct order whenever I reboot the host.

Also, by using the image name without a version specified these containers should be very easy to upgrade to the latest version of nginx, Wordpres (container), and mariadb just by restarting the service. Things could go unexpectedly wrong when vendors deprecate features so whether or not that is actually useful remains to be seen, and I would not use this approach for production code where known-failures are key.

Cannot add WordPress Themes or Plugins

I then had trouble with the WordPress site being unable to reach out to the world and quickly realised I had neglected to link to the frontend network in my docker-compose file. Adding that to the yaml and doing docker-compose down then docker-compose up had WordPress able to pull fresh themes and plugins.

WordPress Redirect Hell

Lastly I had a problem where navigating to /wp-admin resulted in a 301 redirect to wpcontainer/wp-admin/ and I thought this was a problem with WordPress itself but it actually turned out to be a simple misconfiguration in my nginx file. I had neglected to tell it to pass the domain name on to the upstream server which means that when the request hit my wordpress container it had no idea about my domain, mcgrane.uk so all redirections were being sent to the only name it knew – the one I put in the nginx config: wpcontainer.

In the end the fix was very easy, I just had to add proxy_set_header Host $host; to the location block. This is the StackExchange thread.

Linux Service for docker-compose

As always, I found several helpful guides online and ended up creating this file in /etc/systemd/system/docker-mcgraneblog.service

[Unit]
Description=Docker Compose containers for McGrane blog
Requires=docker.service
After=docker.service

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/docker/mcgraneblog
ExecStart=/usr/bin/docker-compose up -d
ExecStop=/usr/bin/docker-compose down
TimeoutStartSec=0

[Install]
WantedBy=multi-user.target

As you can see, I have saved my “production” docker-compose file to /opt/docker/mcgraneblog which is the working directory for this service.