Github Actions, Hugo, and dokku
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.
-
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. ↩︎