I decided to upgrade the VPS from Ubuntu 14.04 to 16.04. At the same time, I decided to use Docker containers to compartmentalize the blog. Docker is very cool technology that does for applications what version control has done for code. It allows you to create a container that has all of the dependencies of an application in one immutable container. This means, for me at least, that it is straightforward to upgrade the different pieces.

I have an nginx container that links to a ghost container. All of the data and configuration files are stored on local volumes which are easily accessible for backup and configuration tunning.

So far it is quite nice! I imagine the real power comes when I try to have multiple web apps running alongside the blog. It will be a simple matter of linking the nginx container to the new apps so it can adequately proxy them. It should be quite an exciting and robust system.


I use CloudA as a hosting provider. I have used them for the past few years and haven't had any problems that I haven't caused myself. I was using a base image of Ubuntu 14.04. I was also investigating docker technology on my local system. After successfully setting up Syncthing in a Docker container I thought it would be a good idea to containerize my website.

The process I decided was to shut down the current image and spin up a brand new image of Ubuntu 16.04. This process was relatively easy and painless. Once I had a proper ssh session, I was able to use a proper terminal instead of the web-based console that was provided.

Installing Docker

Make sure the repository is up to date and that everything is upgraded to the latest version:

$ sudo apt-get update
$ sudo apt-get -y upgrade

Install packages to allow apt to use a repository over HTTPS:

$ sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \

Add Docker’s official GPG key:

$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

Verify that the key fingerprint is 9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88.

$ sudo apt-key fingerprint 0EBFCD88

Add the repository:

$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"

Update the apt package index:

$ sudo apt-get update

Install the latest version of Docker CE. Any existing installation of Docker will be replaced:

$ sudo apt-get install docker-ce

Create the Docker Group

Create the docker group:

$ sudo groupadd docker

Add your user to the docker group:

$ sudo usermod -aG docker $USER

NOTE: $USER is the username that you want to add to the docker group

Verify that you can run docker commands without sudo:

$ docker run hello-world

Creating the Required Images

Create the required folders:

$ mkdir ~/docker
$ mkdir ~/docker/ghost
$ mkdir ~/docker/nginx
$ mkdir -p ~/docker/nginx/{logs,ssl,sites-enabled,static}

Ghost storage container:

$ docker create \
    -v /home/troy/docker/ghost/:/var/lib/ghost/content \
    --name=ghost-storage alpine

Ghost app:

$ docker create \
      --name=ghost \
      --restart always \
      --volumes-from=ghost-storage \
      -e NODE_ENV=production \
      -e url=https://bluebill.net \

NOTE: If you don't set the environment variables in the container Ghost will run in development mode. Also note, you need the proper redirect settings for the nginx configuration otherwise you will encounter some weird redirect errors.

Create the nginx volumes container:

$ docker create \
    -v ~/docker/nginx/logs:/var/log/nginx \
    -v ~/docker/nginx/ssl:/etc/ssl \
    -v ~/docker/nginx/sites-enabled:/etc/nginx/conf.d \
    -v ~/docker/nginx/static:/www/data \
    --name=nginx-storage alpine

Create the nginx container:

$ docker create \
      --name=nginx \
      --restart always \
      --volumes-from=nginx-storage \
      -p 80:80 \
      -p 443:443 \
      --link ghost:ghost \

NOTE: For testing, it might be prudent to leave the --restart flag out for the time being.

Let's Encrypt

I will be using a script called dehydrated to handle the certificate process.

Create the location to store the dehydrated script and configuration files:

$ mkdir ~/docker/dehydrated
$ cd ~/docker/dehydrated

Create a file to store the script:

$ touch dehydrated

Copy the script from the repo:

$ vi dehydrated

Create the config file:

$ touch domains.txt

Copy the contents of the example config file to domains.txt, altering the following:


Create the domains.txt:

$ touch domains.txt
$ vi domains.txt

Add the following line to the domains.txt:

bluebill.net www.bluebill.net

Launch the script:

$ ./dehydrated -c

The certificates are here:


Copy the files to:


NOTE: The reason to copy the files is because symbolic links are not followed if they are outside the docker volumes.


You can add a crontab entry to run the dehydrated script automatically:

$ crontab -e
@weekly dehydrated -c

You will also need an entry to a script that will copy the certificate and private key to the proper spot.

Changes to Nginx to Support Let's Encrypt and Properly redirecting Ghost in production


server {
    server_name www.bluebill.net bluebill.net;

    location '/.well-known/acme-challenge' {
        default_type "text/plain";
        alias        /www/data ;
        autoindex    on;

    #make sure we redirect to anything other than the let's encrypt challenge folder.
    location / {
          return 301 https://$host$request_uri;


server {
    listen ssl http2;
    server_name  www.bluebill.net bluebill.net;

    ssl_certificate /etc/ssl/fullchain.pem;
    ssl_certificate_key /etc/ssl/privkey.pem;

    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;

    ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0

    #Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits
    ssl_dhparam /etc/ssl/certs/dhparam.pem;

    #OCSP Stapling ---
    #fetch OCSP records from URL in ssl_certificate and cache them
    ssl_stapling on;
    ssl_stapling_verify on;

    #enable session resumption to improve https performance
    ssl_session_cache shared:SSL:50m;
    ssl_session_timeout 1d;

    #had to set the proper headers so that ghost will work in production.
    location / {
        proxy_pass http://ghost:2368;

        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header HOST $http_host;
        proxy_set_header X-NginX-Proxy true;

        proxy_redirect off;
        proxy_intercept_errors  on;

Create the nginx volumes container:

$ docker create \
    -v ~/docker/nginx/logs:/var/log/nginx \
    -v ~/docker/nginx/ssl:/etc/ssl \
    -v ~/docker/nginx/sites-enabled:/etc/nginx/conf.d \
    -v ~/docker/nginx/static:/www/data \
    --name=nginx-storage alpine

Create the nginx container:

$ docker create \
      --name=nginx \
      --restart always \
      --volumes-from=nginx-storage \
      -p 80:80 \
      -p 443:443 \
      --link ghost:ghost \
$ docker start ghost nginx