Skip to Content

Making Hugo, Gitlab, and Let's Encrypt play together

I thought I’d try and capture what it took to get this thing going. A static site generator is thing I haven’t toyed with in a long while and all the cool kids are playing with fancy new things I needed to learn. Combining Hugo, Gitlab, and Let’s Encrypt in order to automagically generate a secure site to throw content on wasn’t as straightforward as all the documents would have you believe. (But really, when is it ever as easy as the docs would lead you to believe?)

Much of what futzed around with was Hugo itself and minimo, the theme I chose to use. The minimo theme is exactly what I was after and has a number of tuneables along with great documentation.

Making Hugo Work Locally

The Quick Start guide for hugo is solid and will get you running your first site right away. The steps below reflect me following the guide, adjusting a couple steps to leverage the minimo theme:

$ brew install hugo
$ huge new site qmunic8
$ cd qmunci8
$ git init
$ git submodule add themes/minimo
$ cp themes/minimo/exampleSite/config.toml .

From here I went to making changes to config.toml to my tastes. I specifically avoided updating the baseURL parameter at this point as there were no assets on a site yet. Leaving it alone for now worked best for testing purposes. I found that running the hugo server in a Visual Studio Code terminal window worked quite well. I could keep Safari & Code side-by-side while I worked and watch updates happen on the fly.

I set out to find a few image assests to make it more me, shoving them into static in order to override the minimo theme assets. Learning about the static directory here came in really handy later to get a LetsEncrypt certificate generated. This directory is exactly as it sounds: a place to through things that are not dynamically generated by Hugo, but are always available directly from dynamically generated pages. It’s very useful for replacing assets such as images and css that are in someone else’s theme.

Uploading and Generating a GitLab Page

The Hosting on GitLab guide was a great help. I just skipped the repeat git init step, but otherwise followed this to the letter and in a few minutes my site was up on

From this point forward, making updates to the site are as simple as a commit and push away. When I push to the repo, GitLab’s CI solution sucks in a docker image, pulls in minimo as a git submodule, executes Hugo to generate the site, then deploys the artifact to their webserver. Pretty damn nifty.

Using My Own Domain

Sure, I could just use the domain, but nah. I’ve got a couple domains I still hang on to I want to leverage. Glancing over the Add your custom domain instructions, this is all really easy. I just need an A record, a CNAME, and a TXT record in my domain. I update these records accordingly in the Hover portal and in a couple minutes I’m able to pull up the site from GitLab by my own domain. 💥

Note: I did have to uncheck the Force domains with SSL certificates to use HTTPS box when adding the domain in GitLab cause I didn’t have a certificate yet, but that was about to change.

Hoop Jumping For a Certificate

Now that I have a site working on my own domain, I want a certificate for it. Thanks to the fine folks at Let’s Encrypt I can get one for free but because I don’t have a shell on the webserver itself there are a special hoops to jump through.

The EFF provides a tool called certbot which can help automatically generate a certificate for your site. The problem is it wants to run directly on the webserver your site is hosted on. But hidden within the certbot source is a tool called letsencrypt-auto which can be used to generate a token you put on your site that LetsEncrypt can fetch to determine your site is really yours and then generate a certificate for you.

I was momentarily defeated though upon the first run:

$ ./letsencrypt-auto
Requesting to rerun ./letsencrypt-auto with root privileges...
WARNING: certbot-auto support for this macOS is DEPRECATED!
Please visit to learn how to download a version of
Certbot that is packaged for your system. While an existing version
of certbot-auto may work currently, we have stopped supporting updating
system packages for your system. Please switch to a packaged version
as soon as possible.

Well damn. After some quick searching I find that using the --debug flag is helpful to get around that deprecation warning. Let’s give that a shot:

$ ./letsencrypt-auto --debug
Requesting to rerun ./letsencrypt-auto with root privileges...
Bootstrapping dependencies for macOS... (you can skip this with --no-bootstrap)
Using Homebrew to install dependencies...
Error: Running Homebrew as root is extremely dangerous and no longer supported.
As Homebrew does not drop privileges on installation you would be giving all
build scripts full access to your system.

😒 So apparently this thing wants to install its dependencies via Homebrew and I can skip their installation with the --no-bootstrap flag. What are those dependencies? I quickly grep the package install commands out of letsencrypt-auto and get to work:

$ brew install augeas
$ sudo pip install virtualenv
$ sudo pip install hashin

I’m confident this wasn’t the right way (well, the clean way, anyhow) but it got me unblocked:

$ ./letsencrypt-auto certonly -a manual -d --debug --no-bootstrap
Requesting to rerun ./letsencrypt-auto with root privileges...
Creating virtual environment...
Installing Python packages...
Installation succeeded.
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator manual, Installer None
Enter email address (used for urgent renewal and security notices) (Enter 'c' to

Success! At this point, I followed the script’s instructions, supplying my email address and acknowledging a couple of items, eventually resulting in the following output:

Create a file containing just this data:


And make it available on your web server at this URL:

Press Enter to Continue

Note this is a fudged challenge token and file name for example purposes…

I need to get this file up on the site so LetsEncrypt can verify I actually control my site. Remember that static lesson I was mentioning earlier? This is a perfect use case:

$ mkdir -p static/.well-known/acme-challenge/RcsgpX52UexqDIgrE0VlZc3qwRZexKU
$ echo "g1gj0cFzBBHFBtWO-RcsgpX52UexqDIgrE0VlZc3qwRZexKU-hQ-vWhSSN58
" > static/.well-known/acme-challenge/RcsgpX52UexqDIgrE0VlZc3qwRZexKU/index.html

Commit, push, and verify I see the challenge code when I query the expected URL.

$ curl

Now that I’ve confirmed the challenge code is retriveable, I went back to my terminal window running the letsencrypt-auto process and happily hit the Enter key to see what happened next.

Press Enter to Continue
Waiting for verification...
Cleaning up challenges

 - Congratulations! Your certificate and chain have been saved at:
   Your key file has been saved at:
   Your cert will expire on 2018-09-14. To obtain a new or tweaked
   version of this certificate in the future, simply run
   letsencrypt-auto again. To non-interactively renew *all* of your
   certificates, run "letsencrypt-auto renew"
 - Your account credentials have been saved in your Certbot
   configuration directory at /etc/letsencrypt. You should make a
   secure backup of this folder now. This configuration directory will
   also contain certificates and private keys obtained by Certbot so
   making regular backups of this folder is ideal.
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:
   Donating to EFF:          

MOAR SUCCESS! Now I have a certificate (with a chain) and key I can supply to GitLab and make my site SSL friendly. I cat the content of both these files and place their contents in the Certificate (PEM) and Key (PEM) fields respectively on the domain page within GitLab, save my additions, and presto! it’s ready to go.

$ curl -vX HEAD
Warning: Setting custom HTTP method to HEAD with -X/--request may not work the
Warning: way you want. Consider using -I/--head instead.
* Rebuilt URL to:
*   Trying
* Connected to ( port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* Cipher selection: ALL:!EXPORT:!EXPORT40:!EXPORT56:!aNULL:!LOW:!RC4:@STRENGTH
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/cert.pem
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Client hello (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN, server accepted to use h2
* Server certificate:
*  subject:
*  start date: Jun 16 22:20:15 2018 GMT
*  expire date: Sep 14 22:20:15 2018 GMT
*  subjectAltName: host "" matched cert's ""
*  issuer: C=US; O=Let's Encrypt; CN=Let's Encrypt Authority X3
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x7fbba1800a00)
> Host:
> User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)
> Accept: */*
> Referer:
* Connection state changed (MAX_CONCURRENT_STREAMS updated)!
< HTTP/2 200
< accept-ranges: bytes
< cache-control: max-age=600
< content-type: text/html; charset=utf-8
< expires: Sat, 16 Jun 2018 23:36:45 UTC
< last-modified: Sat, 16 Jun 2018 23:15:19 GMT
< vary: Origin
< content-length: 6926
< date: Sat, 16 Jun 2018 23:26:45 GMT
* transfer closed with 6926 bytes remaining to read
* Closing connection 0
* TLSv1.2 (OUT), TLS alert, Client hello (1):
curl: (18) transfer closed with 6926 bytes remaining to read

That’s it, right? Hot damn.