Hugo is a great tool for static site generation, it’s how this site is being generated. Super fast thanks to Go and very flexible with it’s layout and templating system. What could possibly go wrong.

Despite the feature set Hugo offers, they stopped updating their docker image in 2017, almost 8 years ago. That sucks.

Searching the web for anything Hugo and Docker related brings up the old images from Hugo as well as the new unofficial images by Hugomods. I thought to myself this is great, I can spin up a container with this image, point the /src to my static site I want and have it serve up the pages automatically. While that was a good idea, it turns out the built in HTTP server, hugo server, is exclusively development only because they force all URLs to be local ones, http://localhost:1313/.

Even when you change the baseURL in Hugo’s config.toml, it’s mostly ignored for development.

Solution

While Hugo themselves stopped supporting it and Hugomods doesn’t offer a automatic build system, it’s not all too hard to make your own Docker image to do the basics.

The image needs to have Hugo, obviously, some kind of HTTP Server and a way to monitor for changes to trigger regeneration.

So here is the Dockerfile I came up with.

FROM hugomods/hugo:exts-non-root

First you will notice I am using Hugomods’ Hugo image. This is because they offer up to date Hugo images for Docker and I don’t need to reinvent the wheel here.

USER root
RUN apk add --no-cache caddy inotify-tools

Next it switches over to root to install Caddy and inotify-tools. Caddy is my HTTP Server of choice here, you’ll see why later, and inotify-tools will be used for monitoring some folders and the config file for changes.

COPY ./Caddyfile /etc/caddy/Caddyfile
COPY ./watch.sh /watch.sh
RUN chmod +x /watch.sh

Then it copies the Caddyfile and watch.sh files into the image for use. (You can find these files at the end of the post, as well as the full Dockerfile)

USER hugo
WORKDIR /src
EXPOSE 80
CMD /watch.sh & caddy run --config /etc/caddy/Caddyfile --adapter caddyfile

And lastly, switching back to the hugo user account, set the work directory to /src, expose the HTTP port 80 for Caddy and run the watch script and caddy at the same time.

The Caddyfile file is just a very simple config file that tells Caddy to listen on port 80 and serve everything from /src/public as HTTP using the built in file server module, it’s only 3 lines long which likely beats out any other HTTP Server software for configuration simplicity.

The watch.sh file triggers an initial build at startup.

Complete files

Dockerfile - Click to open

FROM hugomods/hugo:exts-non-root

USER root
RUN apk add --no-cache caddy inotify-tools

# Copy static config files
COPY ./Caddyfile /etc/caddy/Caddyfile
COPY ./watch.sh /watch.sh
RUN chmod +x /watch.sh

USER hugo

WORKDIR /src

EXPOSE 80

CMD /watch.sh & caddy run --config /etc/caddy/Caddyfile --adapter caddyfile

Caddyfile - Click to open

:80
root * /src/public
file_server

watch.sh - Click to open

#!/bin/sh

echo "Starting Hugo build watcher..."

echo "Triggering initial build..."
hugo --minify

while inotifywait -r -e modify,create,delete \
    --exclude '(\.git|public)' /src/content /src/layouts /src/static /src/config.toml; do
    echo "Change detected, rebuilding..."
    hugo --minify
done