Docker, Traefik, and Ghost (oh my)
What type of setup was used before?
Around 2015, I was running Ghost on my Mac Mini as a native installation. Their CLI tool made walking through the steps somewhat easier, but I ran into limitations with the latest version of node.js running on my server being too new for Ghost. It made it really challenging to juggle experimenting with the latest node.js while keeping things like HomeBridge and Ghost happy. There are ways to segregate multiple versions of node.js on a system, but it's arguably ugly to have all that on the host machine, needing preening. and leaving junk behind when versions stop being used.
In 2016, I started playing with static site generators, landing on Hexo for a solution. It relies on a CLI tool to generate files in the right paths, allowing editing to happen to files. Posts can be written and tagged in Markdown format, and the Hexo tools will render static html files to be served up. Combining this with a simple NGINX setup means the fetch and load times for the site on browsers is lightning fast, with next to no server-side processing required.
It looked pretty nice, but trying to decipher the theming system was a dark, deep rabbit hole. I had a sweet theme running for years, but it was not maintained, and I ended up reverting back to default theme shown above rather than continue to try to repair the other theme. Plugins, integrations, and scaffolding are available, but they require intimate knowledge of how Hexo does things. Any working solution took a while to figure out, and it was fragile. There is enjoyment messing with a hobby blog technology, but after a while the distraction from the actual blog content becomes annoying when it's a constant hurdle.
How do you configure encrypted HTTPS traffic only?
About the same time I put up my Hexo site in 2016, major browser providers started applying increasing pressure to stop hosting unencrypted sites (rightfully so). Those of you that may have had the unfortunate experience of trying to obtain and configure X.509 certificates for TLS know that up until then, it was a colossal pain to pay some root authority provider $30-hundreds for a certificate, download the files, manage the private key with the right permissions in the right location, back it up securely, configure whatever web server you were using, and try to set reminders for yourself to rotate them at expiration. It was not fun, and maddening that it cost so much to put micro domains up (since it's just generating cryptographic numbers). Thankfully Let's Encrypt entered the scene in mid-2015, opening pathways for running bots to keep the certificates fresh. I had certbot working for a while, but I moved to Traefik as a cleaner way to handle load balancing and certificate management.
I call it "cleaner" because I still wanted to prevent installing services on my host machine, spraying files and config all over, and making things that much more difficult to maintain. Traefik can run as a service, but it shines when run inside Docker, discovering other services inside Docker, and registering them as entrypoints and services based just on Docker metadata. This allows me to manage the infrastructure as version-controllable code. That just leaves backing up the data they generate and use.
How do I run a Traefik Docker image?
The following docker-compose.yml
file forms the base of my infrastructure. It sets up Traefik as the gateway of my web traffic on ports 80 and 443 for my domain, redirecting any 80 traffic to 443. It also serves as a client to Let's Encrypt via TLS challenge to auto-provision the x.509 certificate for TLS, rotating them automatically before any expire. Free and automatic is tough to beat!
version: "3.7"
services:
traefik:
image: "traefik:v2.2"
restart: "always"
container_name: "traefik"
command:
# - "--log.level=DEBUG"
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.http.address=:80"
- "--entrypoints.https.address=:443"
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
# - "--certificatesresolvers.letsencrypt.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"
- "--certificatesresolvers.letsencrypt.acme.email=myemail@domain.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme.json"
ports:
- "80:80"
- "443:443"
- "18080:8080"
volumes:
- "/private/etc/traefik/:/etc/traefik/"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
labels:
- "traefik.enable=true"
- "traefik.http.routers.https-redirect.entrypoints=http"
- "traefik.http.routers.https-redirect.rule=HostRegexp(`{any:.*}`)"
- "traefik.http.routers.https-redirect.middlewares=https-redirect"
- "traefik.http.middlewares.https-redirect.redirectscheme.scheme=https"
- "traefik.http.middlewares.https-redirect.redirectscheme.permanent=true"
Once it is running, there really isn't any maintenance required. I have a single host, so I bound a single /private/etc/traefik
location to store the acme.json
file containing the certificates. If you were to run more load balancers, you would probably want to configure Traefik to store this in a key-value shared storage.
You'll also see a commented-out line that would enable debug logging for Traefik, and another line that would allow test certificate requests to be made if you are just setting things up. Let's Encrypt will throttle or block your connections if you try to obtain certificates too often, so use the certificateresolvers
accordingly.
How do I run this blog from a Docker image?
Running Ghost within Docker consists of picking the right container image, setting options, mounting a volume to store the data the service generates and uses, and applying the Traefik metadata. This fragment is within the same docker-compose.yml
file that started above.
blog:
image: "ghost:3-alpine"
restart: "always"
container_name: "blog"
ports:
- "2368"
logging:
options:
max-size: "100m"
max-file: "1"
environment:
url: "https://www.mysite.com"
database__client: "sqlite3"
database__connection__filename: "/var/lib/ghost/content/data/ghost.db"
database__useNullAsDefault: "true"
database__debug: "false"
labels:
- "traefik.enable=true"
- "traefik.http.routers.blog.rule=Host(`www.mysite.com`,`mysite.com`)"
- "traefik.http.routers.blog.tls.certresolver=letsencrypt"
- "traefik.http.routers.blog.middlewares=blog-redirectregex"
- "traefik.http.middlewares.blog-redirectregex.redirectregex.regex=^https://mysite.com/(.*)"
- "traefik.http.middlewares.blog-redirectregex.redirectregex.replacement=https://www.mysite.com/$${1}"
- "traefik.http.middlewares.blog-redirectregex.redirectregex.permanent=true"
volumes:
- "/path/to/blog-ghost:/var/lib/ghost/content:rw"
The trickiest part of the Traefik metadata involves redirecting calls from mysite.com
to www.mysite.com
via the traefik.http.middlewares.blog-redirectregex
lines. You'll remember the main Traefik Docker configuration handles redirecting from http
to https
.
Running the whole stack on the host machine is now as simple as docker-compose up -d
.