Setting up the blog

Which blogging platform to choose?


I could have picked:

A quick search shows that WordPress is the dominant blogging platform today. Not surprising, it always has been. But I had used WordPress before and did not like it much. WordPress has many themes and plugins available. Too many. You could spend hours trying out free themes or plugins, maybe even go for paid ones, only to find out they are missing some feature or have some significant drawbacks. For example I found one really nice theme once. However it did not support styling well. I had to manually change the style of headers all over the blog because the header styling setting did not work well.

After searching some more, I stumbled on this blogging platform I had not heard of before: Ghost. It turns out Ghost was founded in 2013, and written in Node.js. I actually do not care if it is written in Node.js or another language. What attracted me was that:

  • It is significantly more recent. That should result in a simpler, cleaner ecosystem of themes and plugins. Here's when each platform was first released:
    • 2013: Ghost
    • 2005: Joomla
    • 2003: WordPress
    • 2000: Drupal
  • Users write articles in Markdown. This ensures the style stays consistent across pages.


My host is a VM machine from Digital Ocean. It has many services running on it:

  • several Node.js apps that serve different services on their own port
  • Apache serving different websites on different local ports
  • Nginx to route requests to port 80 for several different domains to the right local ports.

The value of Docker is that in one command, I can set up a Ghost environment that is separate from the host. It includes Node.js, a NoSQL database, and of course Ghost. I do not have to worry whether this version of Node.js is the same as the ones that my other apps require. I don't have to manually install NoSQL and worry about which version I am installing.

First run

Fortunately there is an official image for Ghost on The documentation gives the following command:

$ docker run -d --name some-ghost ghost

This sets up an environment that runs Ghost, but it is only accessible from the machine serving it. Not very useful!

Make it remotely accessible

Remove the old one:

$ docker rm --force some-ghost

Start a new one with port forwarding:

$ docker run -d --name some-ghost -p 3010:2368 ghost

Now Ghost is available remotely on port 3010: I set up Nginx to redirect requests for on port 80 to local port 3010 and voilà! I can see my blog on /.

Persist content to disk

Now I do not want to lose all my blog data if I ever delete the Docker instance. So I set up volume mapping. Whatever is stored in the Docker image's /var/lib/ghost/content folder is mapped to the host VM's ~/wafrat_blog_data folder. Now I can set up automated back up on this folder.

$ docker run -d --name some-ghost -p 3010:2368 \
    -v ~/wafrat_blog_data:/var/lib/ghost/content \

Fix blog url

The last issue was when I clicked on the Home link on the blog, or if I clicked on the RSS feed link, it would redirect me to http://localhost:2368. In the page settings, I can override the Home url to be /, but it does not fix the RSS feed link. A quick web search led me to this GitHub issue on the Ghost Docker image. It turns out you have to set an environment variable when running it. However it is not documented:

$ docker run -d --name some-ghost -p 3010:2368 \
    -v ~/wafrat_blog_data:/var/lib/ghost/content \
    -e url="" \

Nov 2018 update

After more than a year since I first set it up, it was time to update Ghost. I used the Ghost docker image documentation and this other blog post that describes how to update an image.
The basic idea is that your docker container is running an image, and you can't change that image, not even to update it to a newer version. You have to stop that container, delete it, create a new container based on the image at the desired version.
The Ghost docker container doc says that before upgrading to a new major version, you should update to the latest version of your current major version. I was at 1.8.0, and wanted to upgrade to 2.4.0. The latest at major version 1 was 1.25.0. So I first upgraded to 1.25.0, exported my data to a JSON, then to 2.4.0. I was supposed to import that JSON into the new blog. But it turned out that 2.4.0 was able to read my data folder fine.

Here are the few commands I used:

$ docker container list
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                    NAMES
ee095d09cae6        72ed48f1b5e8        "docker-entrypoint..."   13 months ago       Up 3 months>2368/tcp   some-ghost

$ docker container inspect ee095d09cae6
        "Config": {

$ docker stop ee095d09cae6

$ docker run -d --name some-ghost -p 3010:2368 -v ~/wafrat_blog_data:/var/lib/ghost/content -e url="" ghost:1.25.5
Unable to find image 'ghost:1.25.5' locally
1.25.5: Pulling from library/ghost
Status: Downloaded newer image for ghost:1.25.5
docker: Error response from daemon: Conflict. The container name "/some-ghost" is already in use by container "ee095d09cae6bf769743168093080b2ecc46edc7bf987241d581167814ff9771". You have to remove (or rename) that container to be able to reuse that name.
See 'docker run --help'.

$ docker rm some-ghost

$ docker run -d --name some-ghost -p 3010:2368 -v ~/wafrat_blog_data:/var/lib/ghost/content -e url="" ghost:1.25.5

# At this point I went into the Ghost Admin UI and exported all blog data to a JSON file.

$ docker stop a5946da244b0b84acf19206e4076ef1ca1ac66625046a8af13661cc85dd300d1
root@ubuntu-512mb-sfo1-01:~# docker rm some-ghost

$ docker pull ghost
Using default tag: latest
latest: Pulling from library/ghost
05a0de8ccd4d: Pull complete 
Digest: sha256:a8915865ad01b66edce528dce2811c5e6591ed2bcaeecf4473b1d8266311027e
Status: Downloaded newer image for ghost:latest

$ docker run -d --name some-ghost -p 3010:2368 -v ~/wafrat_blog_data:/var/lib/ghost/content -e url="" ghost



Ghost does not provide analytics or commenting out of the box. It is surprising since most others like WordPress or Medium do. Fortunately the documentation explains how to add them via "app integrations".


This is straightforward. Set up a Google Analytics account, then add the tracking snippet to the blog header in the Code injection tab.

Now let's check that it works. I open with Developer Tools open. See the last line collect?v=...? That's the Analytics beacon. It sends information to Google Analytics: what type of event is being tracked (in this case a page view), what the URL is, what the browser is, etc.




Syntax highlighting

This blog post explains how to add syntax highlighting to Ghost. It uses Prism, a script that looks for <code> elements and colors the content according to the language that you specify. After adding the Prism CSS to our blog header, and the Prism JS to our blog footer, and adding the language to our <code> elements, we go from this:


To this:


Nov 2018 update

Going from version 0.0.1 to 1.15.0 in order to support Dart. It seems like you have to single handedly pick which languages you want to support:


So I opted for:

  • Dart
  • Golang
  • Java
  • Json
  • Kotlin

Sequence diagrams

I have always liked sequence diagrams to illustrate how different parts work together. I use It is a free tool that lets you write sequences in text, then export the resulting diagram to SVG with no watermark.