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:
- It should be fully static (I chose Hugo, mostly because I have used it before and I like Go)
- No CI management, I have enough YAML in my life
- Publishing a new post should be as easy as
git commit -am 'new post' && git push
- The deployment should be dead simple and ideally spun up on different machine in minutes
- Nginx as web server
- I’m allowed to use docker
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.
-
Though I must admit Cloudflare Pages seems kinds cool. ↩︎