I try not to blog too much about running this blog itself. But this is still interesting in how it works.

The past

Before this blog, I ran my blog on an OpenShift "gear". It was also running ghost, and it was OK. But it was really hard to upgrade the blog software.

I had very little freedom on the hosting itself, which meant I couldn't run the Let's Encrypt client there.

Additionally, updating anything was really slow and if there were issues they were hard to fix.

Digital Ocean

Digital Ocean offers VPS (Virtual Private Server) instances. The instance receives a public IP address and you can access it via SSH. You get complete control over your VPS. The draw-back is you have to manage everything yourself.

Luckily they offered boxes with docker pre-installed, making it easy to spin up a VPS and get some docker containers running.

Ghost on Docker

I use "Ghost" to run my blog.

To host ghost I'm running three docker containers:

Why nginx?

The ghost container runs a webserver itself. But nginx is used as a reverse proxy. It's nginx that's facing the outside world.

I do this because it's easier to manager the ssl certificates and do web server updates.

But also because it allows me to run different web applications on the same instance. Based on a sub-domain nginx knows to which web app to forward the requests. For example: blog.aaronlenoir.com and news.aaronlenoir.com.


I have everything I need to set up the box in code, on github.

The hardest part was getting the scripts to set everything up working.


To start from scratch, I must first set up a mysql container:

docker run --name $container_name -e MYSQL_ROOT_PASSWORD=$MYSQL_ROOT_PASSWORD -v $PWD/data/$container_name/var/lib/mysql:/var/lib/mysql -d mysql

But once set up I just start mysql as follows:

docker start $container_name

Note: container_name is just a setting, in my case the container_name is "mysql".


Secondly, ghost must be started. This can be done with a throw-away container connecting to MySql. And a data volume to store images and the themes.

Again, the initial setup script is the most difficult since it will fill in the Ghost configuration file with some settings and it will make sure the required folders exist.

See the configuration script on github

But once it's set up, running works like this:

docker run --rm --name $container_name -v $PWD/data/$container_name/var/lib/ghost:/var/lib/ghost -v $PWD/config/config.js:/var/lib/ghost/config.js -e "NODE_ENV=production" --link $mysql_container_name:mysql -d ghost:0

Here --rm indicates the container will be removed when it stops running. Two volumes are added, one for the config file and one of the data. And with --link $mysql_container_name:mysql I can make sure the ghost instance can access the mysql instance via the hostname "mysql".


Running nginx is similar to ghost:

docker run --rm -v $PWD/nginx:/etc/nginx:ro -v /etc/ssl/news.aaronlenoir.com:/etc/ssl/news.aaronlenoir.com -v /etc/ssl/blog.aaronlenoir.com:/etc/ssl/blog.aaronlenoir.com -p 80:80 -p 443:443 --link news-reader:news-reader --link ghost:ghost --name nginx_proxy -d nginx

A volume points to the nginx configuration. Some other volumes point to my ssl certificates. I link the ghost container (and another container for an unrelated application).

So for nginx, the ghost instance is reachable through the "ghost" hostname.

In the nginx config file:

server {
	server_name blog.aaronlenoir.com;
	listen 443;

	ssl on;
	ssl_certificate		/etc/ssl/blog.aaronlenoir.com/fullchain.pem;
	ssl_certificate_key	/etc/ssl/blog.aaronlenoir.com/privkey.pem;
	add_header Strict-Transport-Security "max-age=31536000" always;

	location / {
		proxy_pass http://ghost:2368;

The server name "blog.aaronlenoir.com" is forwarded to the internal "ghost" container.


To update ghost, nginx or mysql I run:

docker pull nginx
docker pull mysql
docker pull ghost:0

The "ghost:0" indicates I want the latest ghost where the version number starts with 0. This is because I do not want ghost v1 which was rewritten from scratch and probably has compatibility issues (I guess).

Wait for MySql

Important detail to mention:

  • When the ghost container starts, it expects the mysql database to be running. If it's not, ghost won't start either.
    • To fix, after I start the "mysql" container I start a "wait for mysql" container and wait for the container to stop

Easy maintenance

The advantage of using docker is that each part of my site runs in a container. To me, this eases maintenance a bit.

To shut-down everything I have a script down.sh. Then I run update.sh. Then I run: up.sh and hope everything still works (it usually does).

I can also easily run the configuration scripts on a new blank machine that runs docker. I've done it many times for testing purposes.

Let's Encrypt

I also use Let's Encrypt in docker to generate my certificates. The docker command is a bit involved:

docker run --rm -p 80:80 -p 443:443 -v $PWD/etc/letsencrypt:/etc/letsencrypt -v $PWD/var/log/letsencrypt:/var/log/letsencrypt certbot/certbot certonly --standalone --agree-tos -v -c /etc/letsencrypt/cli.ini -d news.aaronlenoir.com
cp $PWD/etc/letsencrypt/live/news.aaronlenoir.com/*.pem /etc/ssl/news.aaronlenoir.com/
docker run --rm -p 80:80 -p 443:443 -v $PWD/etc/letsencrypt:/etc/letsencrypt -v $PWD/var/log/letsencrypt:/var/log/letsencrypt certbot/certbot certonly --standalone --agree-tos -v -c /etc/letsencrypt/cli.ini -d blog.aaronlenoir.com
cp $PWD/etc/letsencrypt/live/blog.aaronlenoir.com/*.pem /etc/ssl/blog.aaronlenoir.com/

As you can see, I need to run it for both news.aaronlenoir.com and blog.aaronlenoir.com seperatly. Additionally I must first stop everything before I run let's encrypt, because it needs those ports for it's client.

Sadly, I currently need to run this manually. I could probably get this into nginx itself ...

But it's just one command every 3 months, so not too bad.


It's a lot easier for me to move hosting, update software and manage my Let's Encrypt certificates after moving from OpenShift to Digital Ocean. I know OpenShift was changing their model to also use docker, so it may not be that different anymore.

Most effort was in creating the configuration scripts that could reliably recreate the entire situation on a blank machine.

But once setup, maintenance was easy. I've been running it for a while and haven't run into any big issues.

It's also nice to know that, because of using nginx, it will be easy to add a new web application on another sub-domain, if I want to. And with Let's Encrypt it will be trivial to add SSL as well.

Oh yeah, the price for all this is $5. In Belgium this results in $6.05 because 21% VAT.

Also, you can pay by topping up your credits using PayPal. Which is a payment system I like a lot.