Skip to main content

The proper way to build a Hugo docker container

·2 mins

When I set out to create this blog, a casual internet search revealed a plethora of sub-optimal ways to build a container.

Hosting a blog generated with hugo requires nothing more than a simple web server and the static HTML files. Without further ado, feast your eyes on the most efficient Hugo Dockerfile in town:

FROM --platform=$BUILDPLATFORM alpine:3.13 as build
RUN apk add --no-cache hugo
WORKDIR /src
COPY . .
RUN --mount=type=cache,target=/tmp/hugo_cache \
    hugo

FROM nginxinc/nginx-unprivileged
COPY --from=build /src/public /usr/share/nginx/html

EXPOSE 8080

Ta-da! This Dockerfile uses the magic of multi-stage builds to produce a final image that only contains the blog, without including Hugo itself. It also runs nginx on port 8080 as the non-root user for that extra bit of security.

But wait, there’s more! This is the most efficient multi-arch build you’ll ever encounter. Why, you ask? The secret sauce is the --platform=$BUILDPLATFORM argument. The docker buildx (buildkit) container builder usually runs each stage with every architecture, even though the generated web resources are not architecture-dependent. Fortunately, the --platform flag allows allows us to tell buildkit to refrain from rerunning the stage with an emulator, resulting in faster builds.

Lastly, we optimize the build process by using --mount=type=cache,target=/tmp/hugo_cache to reuse the Hugo cache between builds. This gives us all the caching benefits of running hugo directly on your host.

Many thanks to Tõnis Tiigi, the buildkit developer who made many of these optimizations possible.