Hugo and Nginx multi-stage build Dockerfile
Tags: docker, hugo
Published: 2018-06-16 02:20:10

---
title: "Hugo and Nginx multi-stage build Dockerfile"
published: true
description: "How to build a multi-stage docker image with Hugo and Nginx"
tags: "docker, hugo"
canonical_url: https://reyes.im/post/docker-hugo-image/
---

_Originally posted on [Reyes](https://reyes.im/post/docker-hugo-image/)._

---

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](https://gohugo.io/)), 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](https://www.docker.com/) [Multi-Stage build](https://docs.docker.com/develop/develop-images/multistage-build/), the first step gets the [Hugo](https://gohugo.io/) 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](https://hub.docker.com/_/nginx/).


# 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.</p>
[Docker](https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds)

# Hugo Build Step

```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
```

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.

``` Dockerfile
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

```Dockerfile
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.


```Dockerfile
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](https://en.wikipedia.org/wiki/Reverse_proxy) installed 

```Dockerfile
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.

```Dockerfile
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

```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](https://traefik.io/) 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.