Geekery of all stripes

Github Actions, Hugo, and dokku

· David Bishop

This blog is generated using Hugo, a static site generator. It’s nice! I imported a bunch of posts from my old Jekyll-powered blog, and it’s been lovely. And I host it using Dokku running on a $10/month Digital Ocean droplet, which actually runs three different websites (and provides some other utility functions for me.)

However, the standard way to deploy a site using Hugo is to make your changes in a directory (that’s hopefully in git and/or being backed up somewhere), then you run hugo, it generates the static html in a public/ subdirectory, which you can then push somewhere else. In my case, I set public/ up as a separate git repository that I could then git push dokku main and it would deploy.

What that looks like in an actual session (with the commands’ output redacted for clarity’s sake):

$ pwd
/Users/dbishop/Blogging/gnuconsulting.com/
$ git add content/posts/new_post.md
$ git commit -m 'I added a new post!'
$ git push origin main # An easy step to forget!
$ hugo # generate the site
$ cd public
$ git add -A . # There will be a bunch of changes here
$ git commit -m 'I added a new post!' # A second commit message for one change 🙃
$ git push dokku main # I use dokku as my remote to disambiguate the repos

As you can see, there are a lot of steps there! And I am old, and forgetful. Let’s see if we can cut that down.

There are two approaches we could take - adding a Makefile/Rakefile/etc., and building up a task that does all of this for us. This isn’t a bad idea! It does still require either setting up the second git repo ahead of time, or automatically building and destroying that each time. And it makes it so that you have to have access to a computer (and one you have at least a modicum of tools installed on) in order to push changes to your blog. I bet we can fix both of those.

The other approach, and the one I took (I know, you already read the subject of the post, you’re very smart) is to use GitHub Actions.

Actions is GitHub’s relatively new CI/CD tool, competing against the likes of Jenkins, Concourse and Travis CI. Why did I choose it over the others? Well, I’ve already used the others, I hadn’t used Actions. Learning is cool!

To use GitHub actions, all you have to do is add a YAML file in a directory called .github/workflows/ in your git repo, and push it. Sweet! Here is what I ended up with - don’t worry, we’ll go through each section in a minute.

name: Publish to Production

on:
  push:
    branches: [ main ]

env:
  DOKKU_HOST: 'blog.gnuconsulting.com'
  DOKKU_USER: 'dokku_user'
  DOKKU_APP_NAME: 'blog'
  DOKKU_PRIVATE_KEY: ${{ secrets.DOKKU_PRIVATE_KEY }}

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
      with:
        submodules: recursive
        fetch-depth: 0

    - name: Setup Hugo
      uses: peaceiris/actions-hugo@v2
      with:
        hugo-version: 'latest'
        extended: true

    - name: Build
      run: hugo

    - name: Deploy
      working-directory: ./public
      run: |
        eval $(ssh-agent -s)
        ssh-add <(echo "${DOKKU_PRIVATE_KEY}")
        touch .static
        tar -c . | ssh -o 'StrictHostKeyChecking=no' ${DOKKU_USER}@${DOKKU_HOST} tar:in ${DOKKU_APP_NAME}        

To start off with, we need to name the Action. Because I have an overblown sense of my own importance, and also I’ve been dealing with infrastructure for over 20 years, this action will be Publish to pRoDUcTiOn. You can call yours whatever you want, as long as it is meaningful to you.

Next we’re going to set some env variables. Note that the env dictionary is special - anything set within it can be referenced as an environment variable, which we do below. I’m using it as a way to centralize the configuration.1

Note! Never, ever, ever put the text of an SSH private key into actual configuration. You can add one to GitHub for use like I do here, by going to https://github.com//<REPO_NAME>/settings/secrets/new and adding it - just copy and paste everything from the BEGIN PRIVATE KEY down to the END, including all of the dashes. Make sure the name of the secret matches the name you use here. I also highly recommend creating a new SSH keypair for this use, to make it easy to rotate in the future and contain the blast damage in case it ever does get leaked.

A quick reminder in case it has been a while - to add a new SSH key on your server running dokku, copy the public key file to the server as /tmp/github.pub, login, and run:

$ dokku ssh-keys:add github /tmp/github.pub

Back to the YAML file. Now that we have the configuration setup, we need to checkout our code. Luckily, checking out the repo that we’re in is super-easy. If you use a submodule for your Hugo theme, remember the submodules: recursive flag!

For the next step, we grab a third-party Action where a kind soul has packaged up hugo for us already. Note the extended: true flag! The Homebrew version of Hugo is the extended version, so you may in fact be using features from it, that will cause your site to build without errors but look ugly as hell. You might then spend thirty minutes trying to figure out what the possible difference could be between the version you’re building locally that looks fine, and the exact same version building in an Action. Ask me how I know!

The next step, Build, we actually run the (newly-installed) hugo command. If you want to add any flags to the build, like --minify or -t <theme>, this is where you do it.

Finally, we are going to tar up the newly-created public/ subdirectory and ship it to your dokku server. Tar, you say? Yes! It works great, and keeps us from having to create another git repo to push our changes into, or build a docker image for a static site, or any other weirdness.

The first two lines load our private SSH key, then we touch the .static file in the new directory to give a hint to Herokuish that all we need is Nginx. And finally, we tar everything up and ship it off.

After all of that, our workflow to publish a new post to the site is down to:

$ pwd
/Users/dbishop/Blogging/gnuconsulting.com/
$ git add content/posts/new_post.md
$ git commit -m 'I added a new post!'
$ git push origin main

Actions takes care of the rest, including emailing you if the build fails for whatever reason.

And I promised you could make changes now without needing to be at your own computer. That’s true! You can use the GitHub web interface to create a new file within content/post/, edit it, and push it into your main branch, all without touching the command-line or requiring a computer with a real text editor.

I do, however, recommend that as a break-glass-in-case-of-emergency sort of thing.

And that’s it. Now go and enjoy your increased automation and decreased reliance on memory. Given the rate at which I write new blog posts, and the amount of time I spent figuring out all of that vs. how much I’ll save with each new publishing cycle, I figure this will all be net-positive roughly the same time they’re building the real Babylon 5.


  1. As an exercise for the reader, what needs to change to make this file completely generic and re-usable across multiple sites? As a hint, not all variables need to be secrets. ↩︎