Dockerised Hugo for Local Development

Following on from last night’s post, I needed a way to run Hugo to build the new entry and deploy it. Since I had to rebuild my environment from scratch I wanted to see if I could run Hugo and Go without installing them locally.

I know Go is unlikely to cause any stability issues, as it installs all its dependencies in the user’s home dir, rather than touching system files but I’m determined in my experiment to keep my new install as clean as possible.

Using some insight I’d gathered from using docker-tizonia a Docker version of Tizonia and using asolera’s Golang minimal Dockerfile image as a base, I was able to put together a minimal Dockerfile that does the following:

  1. Creates a golang based build image to pull down the latest version of Hugo.
  2. Build and install the Hugo binary
  3. Copy the binary to a clean image
  4. Set the image work directory to /site
  5. Expose the Hugo server port 1313
  6. Make Hugo the entry point and default to the help text if I forget to add a command.

The Dockerfile looks something like the following:

FROM golang:1.14.3-alpine3.11 AS build

RUN apk add --no-cache git

ARG HUGO_BUILD_TAGS

RUN go get -v github.com/gohugoio/hugo/source
WORKDIR /go/src/github.com/gohugoio/hugo

RUN go install

RUN apk del git

FROM alpine:3.11

COPY --from=build /go/bin/hugo /usr/bin/hugo

RUN mkdir /site
WORKDIR /site

# Expose port for live server
EXPOSE 1313

ENTRYPOINT ["hugo"]
CMD ["--help"]

Also thanks to jojomi of bits cribbed from their Hugo Dockerfile.

Many of the Hugo Dockerfiles I found would copy the website source to the container in preparation of serving the files from Docker. In my case I’m happy with my plain HTML to continue being served where it is, but didn’t want to lose out on the features you get when you’re using Hugo to develop locally - such as running a test server with live reloading.

With the help of a handy “hugo” wrapper shell script, I was able to fire up Hugo in the container, and serve my local files through a mapped volume with no appreciable difference to how Hugo was running for me before.

The wrapper is as follows:

#!/bin/bash

docker run -it --rm \
    --network host \
    --volume=$(pwd):/site \
    --name hugo \
    $(docker build -q .) "$@";

This wrapper

  1. Runs the necessary Docker command to hook the image into the host network so I can check my changes on http://localhost:1313
  2. Shares the working directory into the expected /site working directory on the image.
  3. Passes in whatever argue I pass in.

I set this Hugo file to executable with chmod u+x hugo and I can now run the automatically updating Hugo server with

./hugo server

Now because the command hugo by itself is used the build the site, I now just pass in a harmless switch like -v (verbose) to build the site without triggering the default --help text.

Finally I use my previous ./deploy script to rsync the files to my host.

The two new files are in my personal-chronicle github repo for any good they can be to anyone, and I’m curious to know if there’s any way I can improve the Docker build to simplify it.

Some questions or areas I think I can improve are:

  1. I’m not sure if the line ARG HUGO_BUILD_TAGS is necessary. It just happened to be there when I finally got it working, after removing other lines that were causing it to fail.
  2. I’m getting the hugo source from github.com/gohugoio/hugo/source when the Hugo documentation says the main repo root is what you’d use to install it. I’m not sure if there was a better way to go get the Hugo project.
  3. I think I’d prefer to freeze the version of Hugo at the current version until I choose to upgrade after testing. I’m not sure how to ‘go get’ a specific version of the git repo.
  4. Is the RUN apk del git line necessary if I’m using a throwaway build image?

The thing that blows me away about Docker and Golang and a lot of modern developer technology is just how much “standing on the shoulders of giants” I’m able to do. Docker is not just a clever idea, but such a well built stack that even with a rudimentary understanding of what I wanted to achieve, I was able to do it with a few lines of code. And the Go ecosystem meant that go get etc.. pulled an entire projects worth of dependencies and built the entire Hugo app inside a black box. This is such a far cry from past experiences I’ve had trying to build software from source that I can only express gratitude for all the hard work donated by so many.

Containers May Save Me From Myself

Over on the Aus Mastodon instance (where I choose to Toot, rather than Tweet) I posted that I’m frustrated over and over again that my Linux experience goes like this:

  1. Install a new Linux distro. Be amazed and surprised just how smooth the experience is, and how little effort there was to get it working.

  2. Get excited for more “Linux” and think “Great, time to try compiling something from scratch for the experience” or “Now I can install that technology stack I was reading about”

  3. Install said stack, or attempt to compile said something.

  4. Fail - due to having picked the wrong distro with the wrong version of Python, or having picked a desktop that runs on Wayland instead of X.

  5. Find workarounds, tutorials on how to compile around the issue, or just instructions on how to install another version of Python. Be successful or not.

  6. Get a notification that the Next Version of my distro is available - and look at all the neat new features and stability it has!

  7. Install the new version and discover some new hellish torment that means that the rock solid stability I’ve b1een enjoying up until this version is gone and no amount of scouring the internet, or trawling the logs will help me figure out how to restore my OS and with it, my sanity.

I’m not sure if step seven happens because of my tinkering in step five. The frequency with which it happens is makes me think it has to be me.

So I find another distro, or I download the installer for the new version, and I backup all my files and I rebuild my machine and repeat from step one. It’s getting tiresome.

So having pinned the problem on myself, I’ve decided this time around I’m going to containerise everything. For those only slightly behind me on the discovery of new technology concepts, containers (or sandboxes (or jails Rubenerd has been using for years)) are a way to put applications in their own little bubbles without access to anything else on your computer2. They help keep everything from rubbing up against each other and getting computer juices everywhere - sort of like social distancing for computer software. Fedora Silverblue is container-based and looks amazing. I have loved using Fedora and learning how to sandbox everything is probably a good skill to learn moving forward. Also, @shlee generously gave me his time to teach me Docker and now I want to keep using it for everything.

BUT… Fedora (and Silverblue) have some downsides for me. Remeber step five? Almost every Linux tutorial or piece of software I’ve ever found anywhere assumes two things: you’re using apt as your package manager, and you’re using Xorg not Wayland as your display server. I was constantly hunting for the ‘dnf’ package, or checking to ensure that the new clipboard manager I was about to use could handle Wayland3. Critically, Docker is a second class citizen in Fedora in favour of Podman, and while Podman might be better in some ways - like Wayland: it’s not what everyone is using. In the end the perpetual dream I have [to use the “superior” technology over the “winning” technology]({{< ref “moving-to-hugo” >}}) had to be put aside, and I’ve settled on Cinnamon flavoured Debian.

Debian is not containered. But it doesn’t insist on making me use Podman instead of Docker like Fedora does, and it’s the closest thing Linux has to a “default” distribution, so I’m making do. The first thing I did was install Flatpak to start my container journey and… immediately failed.

  • Flatpak Firefox looks like shit. I spend almost all my computer time in Firefox and I want it to match my theme and the container version didn’t. That’s a really shallow reason not to use it though, so I’m going to try that again4.

  • Docker is complex enough for a Docker n00b to learn. Trying to run a Sandboxed containerised Docker instance of some sort is right out, so it got a full install.

  • The Flatpak version of VS Code is so isolated it can’t see Docker. I want to use the Docker plugin. I switched to the fully integrated version immediately.

So my ideals took a small beating when the rubber hit the road, but I swear to Woz that I will only use Docker and Flatpak for everything else. And one day when I’m more comfortable translating distro specific nonsense into my preferred flavour, I will give Silverblue or a fully containered distro a much better go.

And maybe one day my desktop will go more than a single major version without being replaced.


  1. While writing this post, this is the literal point that Gnome took one final shit on the bed and decided to freeze within seconds of loading after every hard reboot. 

  2. To avoid the wrath of the pedants, there’s a difference between containers and sandboxes and containers aren’t built for security like sandboxes are, but for my purposes they serve roughly the same function. 

  3. Spolier: it could not. 

  4. Thanks to the Flatpak Theming instructions at OMG! Ubuntu! I was able to install the Adapta-Nokto theme for Flatpak apps and everything is right with the world.