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
COPY . .
RUN --mount=type=cache,target=/tmp/hugo_cache \

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


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.