hfjn

Boring Tech Hugo Deployment

Hi! When rebuilding this blog I strictly set myself the goal to make it’s deployment and maintenance as easy and low-tech as possible. Also I wanted to be able to fully manage it myself and stay away from SaaS solutions like Netlify or Cloudflare Pages1.

So I set myself a few constraints for this blog:

Here’s what I came up with.

The build is just a simple docker multi-stage build that installs the latest version of hugo, copies the content and theme, renders it and copies it over to a fresh nginx image.

 1FROM golang AS build
 2RUN CGO_ENABLED=1 go install -tags extended github.com/gohugoio/hugo@latest
 3WORKDIR /app
 4ADD . /app
 5RUN hugo
 6
 7FROM nginx
 8RUN mkdir -p /var/www/html
 9WORKDIR /var/www/html
10COPY ./nginx.conf /etc/nginx/nginx.conf
11COPY --from=build /app/public /var/www/html

The deployment config is a 16 line docker-compose.yml which besides building and starting up nginx also takes care of starting and configuring certbot to issue the Letsencrypt TLS Certificate.

 1services:
 2  blog:
 3    container_name: blog
 4    build: .
 5    restart: unless-stopped
 6    ports:
 7      - 80:80
 8      - 443:443
 9    volumes:
10      - ./certbot/conf:/etc/letsencrypt
11      - ./certbot/www:/var/www/certbot
12  certbot:
13    image: certbot/certbot
14    container_name: certbot
15    volumes:
16      - ./certbot/conf:/etc/letsencrypt
17      - ./certbot/www:/var/www/certbot
18    command: renew --webroot -w /var/www/certbot

To automate the deployment I have two tiny bash scripts. The first just checks a local clone of my blog repository for diffs against its remote version. In case it finds changes it just drops all local changes, pulls the diff and triggers a redeploy of the blog with the --build flag so the docker build is triggered. The second one just triggers a cert renew and afterwards restarts nginx.

 1#! /usr/bin/env bash
 2set -euxo pipefail
 3cd "${0%/*}"
 4git fetch
 5if ! git diff --quiet --exit-code main...origin/main; then
 6  git reset --hard
 7  git pull
 8  HUGO_PUBLISHDIR=public_tmp hugo
 9  rsync -avh public_tmp/ public/ --delete
10fi
1#! /usr/bin/env bash
2docker compose -f /blog/dir/docker-compose.yml run --rm certbot
3docker compose -f /blog/dir/docker-compose.yml restart blog

Both the scripts are triggered regularly by cron, the first every 5 minutes. The second every sunday at midnight. Additionally the logs are piped to a logfile. Just in case.

5 * * * * /blog/dir/update.sh >> /some/log/file 2>&1
0 0 * * 7 /blog/dir/renew.sh >> /some/log/file 2>&1

And that’s it. A very boring but automated static page deployment in less than 50 lines of code. Let’s see how brittle it is.


  1. Though I must admit Cloudflare Pages seems kinds cool. ↩︎