Hugo Static Site Generator with CI Deployment using GitLab

Over the weekend, I moved my blog from GitHub to GitLab. I now have a fully automated CI build on GitLab that grabs a NodeJS Docker image, downloads Hugo, UglifyCSS and HTMLMinifier via NPM, build and minify pages, and finally publishes to GitLab Pages on every Git merge to master.


I have been using Hugo Static Site Generator for over a year. Initially my blog was hosted on Google Firebase. Not long after, I moved to GitHub Pages and been using it for over a year. Be it Firebase or GitHub, they both worked very well for me.

If they have been working, why fix what is not broken?

TL;DR, to automate chores.

For full explanation, let me start by explaining my old chores of composing and publishing a new blog post:

  1. Firstly, I compose a blog post in Markdown using Visual Studio Code.
  2. Also, run Hugo while composing as it will translate Markdown to HTML on-the-fly ready to viewed. I had the full command line in a batch file.
  3. My blog’s source files are then synchronised to a Git repository on Google Cloud Source Repositories.
  4. When ready to publish, I execute Hugo without parameters to deploy to public folder. Then, minify CSS and HTML files. These commands were also in a batch file.
  5. I then use Free File Sync, an open source folder synchronisation tool, to one-way-mirror the public folder to a local Git clone of my blog’s published content on GitHub. Lastly, commit and push to remote master branch.

I spent the weekend improving the end-to-end process as much as I could.

What, why and how to automate

One Git repository for both source and published content

Table below shows comparison among free tier options:

Google Cloud RepositoryGitHub PagesGitLab Pages
Bandwidth per month10GB100GBUnlimited
Private repositoryYesNoYes
Public repositoryNoYesYes
Static site generatorNoJekyllAny*

*Possible through the use of Docker images for CI builds. See example websites hosted by GitLab Pages.

Which Docker image to use and why

I compared 3 different GitLab CI configurations. Configurations are in YAML format:

  1. GitLab’s example
  2. Hugo’s example
  3. My approach

GitLab’s example

GitLab’s example uses a very small Docker image, Alpine Linux. The size is just 5MB.

The key lines are as follows:

image: alpine
  HUGO_VERSION: '0.23'
  HUGO_SHA: 'c9cf515067f396807161466c9968f10e61f762f49d766215db37b01402ca7ca7'
  - apk update && apk add openssl ca-certificates
  - wget -O ${HUGO_VERSION}.tar.gz${HUGO_VERSION}/hugo_${HUGO_VERSION}_Linux-64bit.tar.gz
  - echo "${HUGO_SHA}  ${HUGO_VERSION}.tar.gz" | sha256sum -c
  - tar xf ${HUGO_VERSION}.tar.gz && mv hugo* /usr/bin/hugo
  - hugo version

The variables need to be configured. They are HUGO_VERSION which is the version to download and HUGO_SHA which is a SHA256 hash of the Hugo release.

It will get you going if you need just the bare minimum.

Hugo’s example

Hugo’s example eliminates the need to maintain the YAML file by not needing to update version and SHA256 checksum of Hugo releases. Its Docker image already has Hugo in it.

image: publysher/hugo

  - hugo
    - public
  - master

This approach is the most simplified option that I could find.

My approach

I setup my .gitlab-ci.yml to use NodeJS Alpine Docker image.

image: node:6.11.2-alpine
  - apk update && apk add openssl ca-certificates
  - npm install
  - PATH=$(npm bin):$PATH
  - hugo version
  - npm run build
    - public
  - master

Undoubtedly more complicated than the other two options but there are reasons.

I leveraged on NodeJS NPM Package Manager to install Hugo binary as well as CSS and HTML minifiers. The version of Hugo on NPM is slightly outdated but this is fine by me. The minifiers are used to shrink files after Hugo has generated them.

From the YAML file above, the build command that GitLab CI will call is npm run build which in turn runs the command found in my package.json. Read next section.

Note: This Docker image does not contain Python. Python is required for Pygments to work. If you have not heard of Pygments, it is a server-side syntax highlighter. To reduce my blog’s dependencies, I chose to replace this with HighlightJS, a client-side alternative.

My package.json file

Below is a JSON file that NPM understands. It is placed at root of your Hugo source folder:

    "name": "",
    "preferGlobal": true,
    "version": "0.0.1",
    "author": "Leow Kah Man",
    "description": "Leow Kah Man - Tech Blog",
    "license": "MIT",
    "repository": {
        "type": "git",
        "url": ""
    "engines": {
        "node": ">=6.00"
    "scripts": {
        "start": "hugo server --theme=leow-kah-man --buildDrafts --disableLiveReload=true",
        "build": "hugo && uglifycss ./public/css/custom.css --output ./public/css/custom.css && html-minifier --collapse-whitespace --keep-closing-slash --file-ext html --remove-comments --minify-css true --minify-js true --input-dir public --output-dir public"
    "dependencies": {
        "html-minifier": "^3.5.3",
        "hugo-bin": "^0.12.0",
        "uglifycss": "0.0.27"
  1. While composing Markdown files, run npm start so that Hugo can generate files as soon as it gets saved.
  2. Run npm run build to generate files using Hugo and then minify them using UglifyCSS and HTMLMinifier. This command should not need to be executed by yourself unless you are testing the build process.

Triggering GitLab CI

All that is left is pushing these along with Hugo source files into master branch on GitLab. The CI is triggered automatically upon performing Git push to GitLab. If the build fails, you will receive an email notification almost immediately. Otherwise, the website should be published in about a minute.

Additional information

You can find additional information, i.e. SSL/TLS setup on custom domains under GitLab Pages Getting Started guide.