Building Ghost in a docker container with Ubuntu, Traefik, and SQLite

My early experiments with installing Ghost presented a curio: it's so light, how do I install without eating a whole VPS server. Or even make use of this one server for more than one Ghost blog, effectively a Ghost multisite.

Building Ghost in a docker container with Ubuntu, Traefik, and SQLite

My early experiments with installing Ghost presented a curio: it's so light, how do I install without eating a whole VPS server. Or even make use of this one server for more than one Ghost blog, effectively a Ghost multisite. The revelation is of course to use Docker containers and broker the inbound traffic, allowing coexistence of multiple apps.

It's a natural fit to get a light Ghost install into a container and run more than one, milking as much performance as possible out of a small virtual server. Fronting it with Traefik is perfect to broker the incoming connections and take care of the HTTPS encryption.

Ubuntu on an AWS Lightsail $5 instance

As an AWS Architect, I must admit with a head full of EC2 I didn't really look at Lightsail very closely, until now. For a long time the Digital Ocean $5 VPS has been my low end server of choice, however on paper with a 2TB transfer allowance, the AWS offering is far better. Beware that the 2TB applies to both data in and data out. Although in fairness to D.O., I don't believe they charge for exceeding their egress limits yet.

The server I spun up in the Oregon region is no doubt EC2 underneath but isolated from the main AWS management console. The spin up proved simple, with a user friendly interface. The only criticism I have so far is that the firewall is primitive, not allowing me to IP whitelist while under development. There is of course the operating system firewall available. (Note IP whitelisting is now available).

I have chosen Ubuntu for no reason other than I have gotten used to it from following various excellent Digital Ocean tutorials over the years, which are frequently based on it. I have the latest long term support version, 18.04.

Linux and Traefik install and configuration

I have followed these great Digital Ocean tutorials to get the base Traefik in place. Please follow them through to get your server ready.

It's worth noting a couple of things about the Traefik tutorial. 1 - there is no example for how to create a docker compose entry for Traefik itself (I'll provide an example now), and 2 - you don't need to go ahead with all the examples in order to get Ghost running. It's wise to experiment on a test server with a subdomain, which you can later delete when you are ready to build your production site.

Standing up Traefik with Docker Compose.

Assuming you are now somewhat familiar with the tutorial topics, a sample docker compose for Traefik, similar to what is built with the Docker Run in the tutorial, could be built in a file with a default name docker-compose.yml like so.

version: "3"
networks:
        web:
                external: true

services:
        traefik:
                image: traefik:maroilles-alpine
                restart: always
                ports:
                        - "80:80"
                        - "443:443"
                volumes:
                        - /var/run/docker.sock:/var/run/docker.sock
                        - /data/traefik/traefik.toml:/traefik.toml
                        - /data/traefik/acme.json:/acme.json
                labels:
                        - traefik.backend=traefik
                        - traefik.frontend.rule=Host:monitor.yourdomain.com
                        - traefik.port=8080
                networks:
                        - web

I always go for an Alpine container version where I can, to reduce the footprint. As both containers are straight out of the core Docker hub without modification, even more so. I have created a /data area on my local disk for mounting volumes containing local data. My intention is to later back up the /data tree to an S3 volume.

Once you are running, on the Traefik monitor page you should see just one Trafik front end service, and one back end service, for itself.

Update 03-2020. Traefik has now moved on from version 1. I have updated these examples to use an image of traefik:maroilles-alpine which should be the "latest Traefik version 1 image" to guarantee compatibility with these configs. This can be verified on docker hub here.

Launching a Ghost container with SQLite under Traefik

It's not always apparent, but you don't actually need to spin up a database to go with a Ghost instance. This default method will implement a Ghost instance with a self contained, file based SQLite database. Unless you are implementing a heavy traffic blog with heavy admin panel use, it's unlikely the speed and simplicity of this combination can be beaten. As it's file based, the precious memory that a database would otherwise eat is saved.

        myghostblog:
                image: ghost:3.11-alpine
                restart: always
                environment:
                        - url=https://mydomain.com
                volumes:
                        - /data/myghostdir:/var/lib/ghost/content
                labels:
                        - traefik.backend=myghostblog
                        - traefik.frontend.rule=Host:mydomain.com
                        - traefik.docker.network=web
                        - traefik.port=2368
                networks:
                        - web

Substitute myghostblog with your blog instance friendly name, and replace mydomain.com with your blog domain. Remember that YAML format files are indentation sensitive.

In the past I have changed the blog URL simply by changing the two domain fields and reapplying the compose. After logging back in on the new URLs, the blog content is all still intact. While there may be an impact if you have Ghost integrations, it was otherwise unbelievably easy.

Choosing a Ghost image

Bear in mind that by choosing ghost:alpine image in my example above, I am choosing to always pull the latest image. There is some risk associated with this as it is automatically upgrading every time you pull the image. Right now ghost:alpine is a link to the latest ghost:2.19.3-alpine Ghost version. Tomorrow, that may link to a newer version, or even a version 3.

If you are risk averse, you should link to the version controlled image at ghost:2.19.3-alpine (or whatever the current one is) but then you must monitor releases and manually upgrade.

It's also possible to link to more major image versions such as ghost:2-alpine or ghost:2.19-alpine. To see all of the current Ghost versions, consult the official Ghost page here: https://hub.docker.com/_/ghost

If you are not sure, just go for ghost:2-alpine.

Update 03-2020. I have found Database issues with automatically updating Ghost via container when using the embedded database, even when the DB datafiles are externalised. I have decided to only allow minor updates automatically, so now use ghost:3.11-alpine If you would like to use version 2, the last version at this time is 2.38-alpine, and version 1, 1.26-alpine. Whichever version, you should keep up to date for security reasons.

Update 07-2020. There is a woefully out of date upgrade process for a Ghost container on the Docker Hub page, but due to problems with auto-update, now I have chosen to always "go manual" for any Ghost container upgrades higher that the lowest point version. This roughly is:

  1. Check any post drafts are saved from the admin panel
  2. Snapshot backup of server
  3. JSON Export of the blog - found under Labs in the admin panel
  4. docker-compose stop myghostblog (replace that last with your compose name for the blog)
  5. Edit docker-compose.yml
  6. Change, in my case the line that says, image: ghost:3.11-alpine to the latest version while allowing minor version updates: 3.22-alpine
  7. Save and close the compose file
  8. Run docker-compose up -d
  9. Restart, docker-compose restart myghostblog
  10. Check the About page shows the new version. In the admin panel click your name bottom left and "About"

Updating Docker image versions

Periodically you can pull an updated version, which would pull minor updates, e.g. from 3.11.1 to 3.11.2. To do this manually:

docker pull ghost:3.11-alpine
docker pull traefik:maroilles-alpine
docker-compose up -d

Optionally, I would also recommend looking into an automatic image update. I have an article on it here.

Docker data directory to house attached volumes

The final /data directory contains:

/data/myghostdir
/data/traefik

Importantly, this allows me to later go into /data and do a file-level backup. Should I ever want to clone the server, because I have also put my docker-compose.yml file under /data/traefik I can quickly port the contents from one server to another which is excellent for recovery or spinning up a temporary server for testing.

/data/myghostdir encapsulates all of the variable parts of the blog, including the SQLite database with the blog entries.

"Ghost multisite" achieved with multiple containers

It's easy to see now how you can stack multiple ghost instances. Adding further containers to the same stack will allow coexistence with a low memory footprint, and Traefik brokering all of the incoming domain calls, handling HTTPS termination with Let's Encrypt under the bonnet.

The above deployment with Traefik and a single Ghost instance shows 350MB of memory used on my $5 server which has a total of 1GB. That's a capacious 650MB left over for file system buffering, or filling up with more Ghost bloggy goodness.

I have experimented to see just how many blogs I can stuff into that leftover space in this article - "Filling up a 1GB server with a stack of Ghost blogs".

Complete example of docker-compose.yml with Traefik, Ghost

version: "3"
networks:
        web:
                external: true

services:
        traefik:
                image: traefik:maroilles-alpine
                restart: always
                ports:
                        - "80:80"
                        - "443:443"
                volumes:
                        - /var/run/docker.sock:/var/run/docker.sock
                        - /data/traefik/traefik.toml:/traefik.toml
                        - /data/traefik/acme.json:/acme.json
                labels:
                        - traefik.backend=traefik
                        - traefik.frontend.rule=Host:monitor.yourdomain.com
                        - traefik.port=8080
                networks:
                        - web
        myghostblog:
                image: ghost:3.11-alpine
                restart: always
                environment:
                        - url=https://mydomain.com
                volumes:
                        - /data/myghostdir:/var/lib/ghost/content
                labels:
                        - traefik.backend=myghostblog
                        - traefik.frontend.rule=Host:mydomain.com
                        - traefik.docker.network=web
                        - traefik.port=2368
                networks:
                        - web

Main photo courtesy of frank mckenna on Unsplash

💬
Your comments are welcome. Please COMMENT and read those of others on the Bluesky Post for this article.

Retrospective blog post to use BlueSky for comments: techroads.org/building-gho...

[image or embed]

— TechRoads blog (@techroads.org) Feb 22, 2024 at 11:24 am