Why & How Ghost

This blog post documents my journey in setting up my blog. From the decision of which platform to use, to the rollout using docker-compose.

Why & How Ghost
Photo by Drew Tilk / Unsplash

To make life for myself easier, and maybe to help people, I will be documenting all my projects on this site. This first entry will be about how I got this blog running.

Decisions

The first step was the most difficult: deciding what platform to use. After researching and testing multiple tools, I chose Ghost. It was a close call between Ghost and WriteFreely. And while Ghost has more than I was looking for, WriteFreely missed two important (for me) features.

  1. It doesn't have a production-ready container image. And while I could have made one myself, there was still issue number 2.
  2. No search/categories/tags. While this isn't an issue for a simple blog, it makes it hard to find the right posts.

Architecture

Ghost doesn't need much. A server to run on, a database to keep data and a reverse proxy to handle requests and ssl.

I decided to run it using Docker. I'm a fan of containers, they make life easy. And although there are alternatives available (Podman, Kubernetes, ...). Docker (and docker-compose) is all I need and what I know best.

The data is kept in a sqlite database. I could have gone for a MySQL instance. But I like to keep things simple. And I don't expect performance to be an issue.

Lastly there is Nginx Proxy Manager to be used as a revers proxy and handle ssl certificates. How I got this set up will be its own post soon (I hope).

Implementation

To get things up and running I created an empty directory. In here are two files, docker-compose.yml and .env.

Compose

The first part of the docker-compose.yml file contains the following:

version: "3.1"

networks:
  default:
    external:
      name: npm_network

As you can see, the docker-compose.yml file contains 3 parts: version, networks and services.

I use version: "3.1" because that's what the official documentation tells me.

The network part specifies to use the external docker network npm_network as default for this compose file. The npm_network is a network to which all my services are connected. This allows me to use Nginx Reverse Proxy to connect directly to the container. Because of this I don't have to expose a port to my host. I will show you how to create this network in my next post.

If you don't want to wait until the second part. Or if you want to use a different (or no) reverse proxy, you'll have to expose the port in the services part. You will also need to delete the network part from the compose file.

The next part contains the service itself:

services:
  ghost-app:
    image: ghost:4-alpine
    volumes:
      - ${DATADIR}/app:/var/lib/ghost/content
    restart: unless-stopped
    environment:
      url: ${APP_URL}
      database__client: sqlite3
      useMinFiles: "true"
      compress: "true"
      imageOptimization: "true"

This defines a single service called ghost-app. It uses the official ghost:4-alpine image. This is version 4 of Ghost, on an alpine-based base image. Using alpine makes the image smaller. It hasn't has as many tools as some other base images, but as we won't be manipulating the image that's no issue.

restart: unless-stopped means the container will be restarted if something goes wrong.

We specify one volume. This volume will contain all the data Ghost needs. So we data will stay persistent when something goes wrong with the container. This also means we can backup the data from the host. And backups are important!

I chose a few environment variables to configure ghost. More info about all available parameters can be found here. url tells ghost what the base URL of the instance will be. By default the image should use sqlite, but just to be sure I defined it in the variables. useMinFiles, compress and imageOptimization are three parameters I'm using in my conquest to keep traffic between my server and your browser to a minimum.

Variables

As you can see in the compose file, we make use of some variables. This is because I host all my compose files on GitHub and I don't want people to know my little secrets.

To fill in these variables we put a .env file in the folder. This file contains the variables:

DATADIR=/media/docker/ghost
APP_URL=https://ghost.krokantekrab.be

for this once you can see what I've put in the file. The APP_URL is this website, and DATADIR is a folder on the host in which the data is stored.

Launch

Using docker-compose config you can see how the result will look with all variables filled in:

root@jveweb:/opt/DockerStacks/ghost# docker-compose config
networks:
  default:
    external:
      name: npm_network
services:
  ghost-app:
    environment:
      compress: "true"
      database__client: sqlite3
      imageOptimization: "true"
      url: https://ghost.krokantekrab.be
      useMinFiles: "true"
    image: ghost:4-alpine
    restart: unless-stopped
    volumes:
    - /media/docker/ghost/app:/var/lib/ghost/content:rw
version: '3.1'

Now just run docker-compose up -d and the service will be started in the background. Configure your reverse proxy (or wait for my next article) and you're done!

And that's it! Thats how what you're see is running. I hope you found it useful. If you got questions, don't hesitate to contact me!