Hugo and Nginx multi-stage build Dockerfile

After searching for a bit I was unable to find a nice pre-made Dockerfile to serve my personal site (built on top of Hugo), some of the images I found were only Hugo build steps, some others were able to serve and build the site but they pulled the FROM:ubuntu docker anti-pattern.

So here I’ll describe what I’m doing on my final Dockerfile, it’s a really simple Docker Multi-Stage build, the first step gets the Hugo binary and builds the site, the second one copies over the public folder of the built site and serves it using the official alpine Nginx image.

What’s a multi-stage build?

With multi-stage builds, you use multiple FROM statements in your Dockerfile. Each FROM instruction can use a different base, and each of them begins a new stage of the build. You can selectively copy artifacts from one stage to another, leaving behind everything you don’t want in the final image. To show how this works, Let’s adapt the Dockerfile from the previous section to use multi-stage builds.

Docker

Hugo Build Step

FROM alpine:3.5 as build

ENV HUGO_VERSION 0.41
ENV HUGO_BINARY hugo_${HUGO_VERSION}_Linux-64bit.tar.gz

# Install Hugo
RUN set -x && \
  apk add --update wget ca-certificates && \
  wget https://github.com/spf13/hugo/releases/download/v${HUGO_VERSION}/${HUGO_BINARY} && \
  tar xzf ${HUGO_BINARY} && \
  rm -r ${HUGO_BINARY} && \
  mv hugo /usr/bin && \
  apk del wget ca-certificates && \
  rm /var/cache/apk/*

COPY ./ /site

WORKDIR /site

RUN /usr/bin/hugo

The first part FROM alpine:3.5 as build it’s labeling that build step as build so that in the next step we can get artifacts from this previous image, the artifact we’ll be keeping is the built site.

ENV HUGO_VERSION 0.41
ENV HUGO_BINARY hugo_${HUGO_VERSION}_Linux-64bit.tar.gz

This is pretty self-explanatory, declare the Hugo version and use it to get the binary name that we’re gonna get from the official GitHub releases

RUN set -x && \
  apk add --update wget ca-certificates && \
  wget https://github.com/spf13/hugo/releases/download/v${HUGO_VERSION}/${HUGO_BINARY} && \
  tar xzf ${HUGO_BINARY} && \
  rm -r ${HUGO_BINARY} && \
  mv hugo /usr/bin && \
  apk del wget ca-certificates && \
  rm /var/cache/apk/*

set -x will give us a nice output for these commands, then we are updating the repos and installing wget and ca-certificates from the alpine repositories, after that we’re building the release binary download URL from the GitHub releases page and downloading it using wget, then we’re uncompressing the tar file, deleting it and moving the binary to the /usr/bin folder, after that we do some standard clean-up tasks to get the final image to the lower size we can without much effort.

COPY ./ /site

WORKDIR /site

RUN /usr/bin/hugo

Now we’re copying the current directory (should be the Hugo site root folder) and moving it into /site (inside the container), next we’re basically doing a change-dir to /site and the last step is the one that actually runs the Hugo binary against /site and that gets us the final /public folder with the assets that we’re gonna server over Nginx.

Nginx Build Step

This one is pretty simple and it’s the second and last step, we’re gonna be using the official Alpine Nginx repo to get a container with the reverse-proxy installed

FROM nginx:alpine

LABEL maintainer Eduardo Reyes <eduardo@reyes.im>

COPY ./conf/default.conf /etc/nginx/conf.d/default.conf

COPY --from=build /site/public /var/www/site

WORKDIR /var/www/site

This one is really simple, we’re getting the official nginx:alpine image and using it as our second build step, then I’m adding my maintainer label to the final container, copying over a Nginx configuration file over the container, the important step is this one.

COPY --from=build /site/public /var/www/site

This step is getting the public folder (the artifact we built on the first step) --from the build step of the multi-stage build and copying it over /var/www/site were Nginx will serve it as static content thanks to the basic configuration file copied earlier and then we’re changing dirs into the site, so that if we want to go into the container we’re already where the code is.

Final Dockerfile

FROM alpine:3.5 as build

ENV HUGO_VERSION 0.41
ENV HUGO_BINARY hugo_${HUGO_VERSION}_Linux-64bit.tar.gz

# Install Hugo
RUN set -x && \
  apk add --update wget ca-certificates && \
  wget https://github.com/spf13/hugo/releases/download/v${HUGO_VERSION}/${HUGO_BINARY} && \
  tar xzf ${HUGO_BINARY} && \
  rm -r ${HUGO_BINARY} && \
  mv hugo /usr/bin && \
  apk del wget ca-certificates && \
  rm /var/cache/apk/*

COPY ./ /site

WORKDIR /site

RUN /usr/bin/hugo

FROM nginx:alpine

LABEL maintainer Eduardo Reyes <eduardo@reyes.im>

COPY ./conf/default.conf /etc/nginx/conf.d/default.conf

COPY --from=build /site/public /var/www/site

WORKDIR /var/www/site

Building the image

This one is a really simple docker command, you should run it from the root of your Hugo site.

docker build -t some-Hugo-site .

and to see the results just do:

docker run -d -p 8080:80 some-hugo-site

I recommend using traefik to expose the container to the web, take advantage of the automatic let’s encrypt integration and add a http -> https redirect to get a nice TLS workflow.

comments powered by Disqus