Running Hugo server with Docker, proxyfied by Traefik

This article is explaining a little bit what technology is behind this website.

Playing with new technology, concept and tools is always an enriching experience.

With soon more than 20 years of experience in Linux servers, i started to work before the acronym LAMP was created.

Things change fast, and move forward, now we have docker which allows to run tons of predefined containers on one server with a few specific commands.

Setuping a working framework of tools for small companies was never that easy.

Hugo and hugo server

hugo is a static website generator. Tons of documentation can be found on gohugo official website.

Its task is to transform an input of source files and produce a set of html files as output. The content of the website is generated once, often offline, which is ideal for serving content that is not dependant of visitor’s behaviour.

As long as my company website just serves the purpose to spread interesting and usefull content, hugo is fully serving my needs.

That way i can write content in Markdown language and use my favourite tools (Vim, VScode, Intellij Idea) to do so, and source control my website with git . (currently vscode really please me on my office desktop but anyway im disgressing)

One interesting feature of hugo is the embeded http server which can be launched with the hugo server command in a shell, inside a working directory. This helps a lot when designing hugo template and css, to have an interactive live view with your written code.

But this is just an http server, like Apache, Nginx, or younger Caddy.

In fact, in server mode, hugo by default doesn’t write any file on disk. Everything is generated and keept in RAM. It is also monitoring for any modifications of source files, and regenerate the static website on the fly.

Beside a small memory footprint, having everything in RAM means better IO and general performance. As this is pure static content, the Time To First Byte (Google TTFB) should be very low.

I could have run directly hugo server the public address of my hosted server.

But i have several others application (jenkins/gitlab/portainer) inside containers and one public IP address.

How to solve this issue ?

Here comes Traefik

Traefik is a reverse proxy. It is routing all incoming http request received from the public address of the server, and relays those requets to the targeted service running privately on the server, following some rules.

It also deals with encryption, managing the TLS layers, and doing autonomously his own business with Let’s encrypt to get valid TLS certificate.

Traefik runs inside a container, and is using traefik.toml as a config file. Using docker-compose for creating a working stack, the Traefick section is as follow:

services:
  proxy:
    image: traefik:v1.7.16
    networks:
      - traefik
    ports:
      - "80:80"
      - "8080:8080"
      - "443:443"
    volumes:
      -  "$PWD/traefik.toml:/etc/traefik/traefik.toml"
      -  "$PWD/acme:/etc/traefik/acme"
      - /var/run/docker.sock:/var/run/docker.sock
    restart: unless-stopped
    labels:
      - "traefik.frontend.rule=Host:traefik.citoyx.com"
      - "traefik.port=8080"
      - "traefik.backend=traefik"
      - "traefik.frontend.entryPoints=http,https"

In short

  • we ask for using the docker image of traefik with version 1.7.16.
  • we define a private network tagged traefik
  • we expose ports 80/8080/443 from public ip to the traefik container
  • we mount some volume inside the container, with traefik.toml config file, certificate repository in acme and the docker unix socket on the global system.
  • we create some label tagged traefik which gives routing rules for reverse proxying.

The traefik.toml file is as follow:

# traefik.toml
################################################################
# Global configuration
################################################################


insecureSkipVerify = true
defaultEntryPoints = ["http", "https"]

[entryPoints]
  [entryPoints.http]
  address = ":80"
    [entryPoints.http.redirect]
    entryPoint = "https"
  [entryPoints.https]
  address = ":443"
    [entryPoints.https.tls]

[acme]
email = "tech@citoyx.com"
storageFile = "/etc/traefik/acme/acme.json"
entryPoint = "https"
OnHostRule = true
onDemand = true

[[acme.domains]]
  main = "citoyx.com"
  sans = ["portainer.citoyx.com", "traefik.citoyx.com", "git.citoyx.com"]


[acme.httpChallenge]
entryPoint= "http"

[web]
address = ":8080"
[web.auth.basic]
  users = ["dpac:$xxxxxxxxxxxxxxxxxxxxxxx"]

[docker]
endpoint = "unix:///var/run/docker.sock"
domain = "citoyx.com"
watch = true
exposedbydefault = true

The acme part is dealing with let's encrypt to generate valid certificates. But in fact, no much need to declare subdomains here, traefik deals with them in live. (my jenkins is not part of this config, but as a valid certificate for https://jenkins.citoyx.com

Once this configuration is done, the traefik part is done.

Dockerizing Hugo

It already exists several hugo containers. After playing with them a little, i decided to create mine. Mostly cause my aim was to have a container ready to serve file in a production environment, where most of the others containers are made for developpement purpose.

For exemple there is a livereload feature, which will force reload a webpage on the client browser when is it changed. This feature is not required in production. I also wanted to minify the generated content, to produce the smallest html file, to gain more performance.

All this lead to the creation of my own hugo image which can be found on here or directly pulled from Docker hub by running docker pull citoyx/hugo

The docker file is not very creative, i just modifyed some other project docker file to fill my need:

FROM alpine:3.9

LABEL maintainer='dpac_@_citoyx.com'

ENV HUGO_VERSION=0.58.3 \
    HUGO_SITE=/srv/hugo \
    HUGO_BASEURL=http://localhost/ \
    HUGO_ENV=production

RUN apk --no-cache add \
        curl \
        git \
    && curl -SL https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_${HUGO_VERSION}_Linux-64bit.tar.gz \
        -o /tmp/hugo.tar.gz \
    && tar -xzf /tmp/hugo.tar.gz -C /tmp \
    && mv /tmp/hugo /usr/local/bin/ \
    && apk del curl \
    && mkdir -p ${HUGO_SITE} \
    && rm -rf /tmp/*

WORKDIR ${HUGO_SITE}

VOLUME ${HUGO_SITE}

EXPOSE 1313

CMD hugo server \
    --bind 0.0.0.0 \
    --navigateToChanged \
    --templateMetrics \
    --buildDrafts \
    --baseURL ${HUGO_BASEURL} \
    --appendPort=false\
    --minify \
    --disableLiveReload\
    --gc

Im not sure it is the best command line to achieve my goals, if any hugo expert or developpers are reading, i would be pleased to hear from them any kind of comment or improvement.

Once the image is created, it can be used with docker-compose as follow:

  www:
    image: citoyx/hugo
    networks:
      - traefik
    labels:
      - "traefik.frontend.rule=Host:www.citoyx.com"
      - "traefik.port=1313"
      - "traefik.backend=hugo"
      - "traefik.frontend.entryPoints=http,https"
    volumes:
      - "/data/work/hugo:/srv/hugo"
    environment:
      - HUGO_BASEURL=https://www.citoyx.com
    ports:
      - "1313:1313"
    restart: unless-stopped

The most important point is the labels definition, which give instructions to traefik on how to route incoming traffic to this container.

We also declare an environment variable called HUGO_BASEURL, so that all internal url of the generated website will use the public name of the website instead of the 127.0.0.1 by default.

We also mount the directory /data/work/hugo as /srv/hugo inside the container, as defined in the docker image. /data/work/hugo is where all the input file used by hugo are located on the server. Replace it by your own hugo working directory.

Once we are here, we are ready to go.

full code and run

The full docker-compose.yml is as follow:

version: '2'

services:
  proxy:
    image: traefik:v1.7.16
    networks:
      - traefik
    ports:
      - "80:80"
      - "8080:8080"
      - "443:443"
    volumes:
      -  "$PWD/traefik.toml:/etc/traefik/traefik.toml"
      -  "$PWD/acme:/etc/traefik/acme"
      - /var/run/docker.sock:/var/run/docker.sock
    restart: unless-stopped
    labels:
      - "traefik.frontend.rule=Host:traefik.citoyx.com"
      - "traefik.port=8080"
      - "traefik.backend=traefik"
      - "traefik.frontend.entryPoints=http,https"

  portainer:
    image: portainer/portainer
    networks:
      - traefik
    labels:
      - "traefik.frontend.rule=Host:portainer.citoyx.com"
      - "traefik.port=9000"
      - "traefik.backend=portainer"
      - "traefik.frontend.entryPoints=http,https"
    volumes:
        - "/var/run/docker.sock:/var/run/docker.sock"
    restart: unless-stopped
  www:
    image: citoyx/hugo
    networks:
      - traefik
    labels:
      - "traefik.frontend.rule=Host:www.citoyx.com,citoyx.com"
      - "traefik.port=1313"
      - "traefik.backend=hugo"
      - "traefik.frontend.entryPoints=http,https"
    volumes:
      - "/data/work/hugo:/srv/hugo"
    environment:
      - HUGO_BASEURL=https://www.citoyx.com
    ports:
      - "1313:1313"
    restart: unless-stopped

networks:
  traefik:
    external:
      name: traefik

Save it, and just run docker-compose up -d where you saved the file. Connect to your website url. And TADA.

Happy hacking !!!