This post is my first published using GitHub Actions. It was remarkably easy to setup, and now the instant I write a new post and push to main, I’ll be seeing it on here within 30 seconds. Below I’ll explain how I got this working.

Laying the Groundwork

The first step is ensuring the hugo site can be deployed somewhere. In my case, it’s a home server and the stack is:

  • Traefik reverse proxy
  • Dockerized nginx instance, serving the static output from Hugo

The simplest way to deploy this setup is to simply drop the static site generated by the hugo command into the file structure and let the nginx container serve it. However, since I’m using GitHub as my version control system I wanted a way to deploy site changes as I checked them in to GitHub.

Deployment Target

Automation using GitHub Action is a great solution, but first I needed to allow those Actions access to my home server. Putting on my Linux sysadmin hat:

  • Create a new user on my home server
  • Set up ssh for that user (always use public key auth for this)
  • In that user’s home directory, create a public/ folder which is going to receive the hugo site output
  • Ensure the user which runs docker on the machine and the new user I’ve created here both have access to that public/ folder (done using groups).

Note that the access for the docker user can be limited to read-only. The nginx container will mount the content read-only.

A good test is to ensure that, from outside your home network, that this new user can ssh into the machine. I setup separate keys for this, added the public key of my iPhone to the authorized_keys of the new user and confirmed I could log-in via ssh to that user’s home directory.

Now that I have a user whose only job is to accept the generated site incoming from GitHub’s Actions I can move on to setting up the GitHub Action config.

GitHub Actions Setup

When you access your GitHub repository via the web you will see an “Actions” tab. You want to create a new workflow. Note, private repositories work fine!

Let me start with an overview of the GitHub Action script.

name: CI
on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/[email protected]

      - name: Hugo setup
        uses: peaceiris/[email protected]
          
      - name: Build
        run: hugo --environment production
        
      - name: Deploy via Rsync Deployments Action
        uses: Burnett01/[email protected]

      - name: Discord notification
        uses: Ilshidur/[email protected]

I have greatly simplified this and removed a bunch of key information. I’ll cover each section individually.

The first few lines are simple and straightforward. You give this workflow a name and identify what conditions trigger it. In my case, a push to the main branch causes this workflow to run. GitHub Actions offers many choices for events which can trigger your workflow.

The jobs section defines just one job in this case, called deploy. That deploy job is going to run several steps in a series - checking out the code, generating the site, and finally deploying it to my home server. Let’s take them one by one.

Checking out the repo

      - uses: actions/[email protected]
        with:
          submodules: recursive

This leverages the standard GitHub Actions checkout action. I have added the with submodules piece to ensure the theme I am using, hugo-paper, is updated each time this step runs.

Setup and build the hugo site

      - name: Hugo setup
        uses: peaceiris/[email protected]
        with:
          hugo-version: latest
          extended: false

This step uses the awesome peaceiris/actions-hugo GitHub Action. It’s very fast, and I have not found any issues to overcome with my initial setup.

      - name: Build
        run: hugo --environment production

The next step uses that installed version of hugo and builds the production version of my site. I have split my hugo config between production and development, as described in their documentation. Having a split config allows me to set a few variables differently when running on my local machine, such as the baseURL and whether or not the build process generates drafts and future-dated posts.

Move the built hugo site to my home server

      - name: Deploy via Rsync Deployments Action
        uses: Burnett01/[email protected]
        with:
          switches: -avzr --delete
          path: public/
          remote_path: public/
          remote_host: ...
          remote_user: ...
          remote_key: ${{ secrets.DEPLOY_KEY }}

Using rsync, of course.

The switches are standard fare, and the --delete flag removes the remote public directory when copying over new files. If you don’t delete on the remote side of things, files or folders you have removed will not be generated by Hugo (e.g. that post you deleted) but when copying them to the output destination the folder will not be deleted, just remain unchanged.

The local public/ path is the Hugo standard output directory. The remote path is named the same, purely for convenience on my end. I could have named it anything I desired as long as I ensure this deployment action has the correct name and that my docker configuration points to the right place.

Remote host and user should be set based on the earlier step of setting up that deployment target. The host should be a valid domain name for your site and DNS should resolve it correctly (I end up using dynamic DNS since I’m sitting here on a home network connection with a rotating IP address).

Finally, the DEPLOY_KEY variable is setup in my GitHub repository as a secret. More info on GitHub Secrets can be found here, but the simple way to get this working is:

  • Open your GitHub repo on the web
  • Navigate to the Settings tab
  • Click Secrets in the left nav
  • Give your secret a name
  • Paste your secret into the big text box

In this case, the DEPLOY_KEY secret is an SSH private key created just for the purpose of deploying this site. Don’t reuse SSH keys!

Notify me!

      - name: Discord notification
        env:
          DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
        uses: Ilshidur/[email protected]
        with:
          args: 'The project {{ EVENT_PAYLOAD.repository.full_name }} has been deployed.'

I like having this, but it’s optional and doesn’t impact your actual site deployment. I setup a second secret DISCORD_WEBHOOK which is pushed to using the GitHub Action Ilshidur/action-discord. Now when I push a change to the main branch, I get an alert ~30 seconds later telling me the deployment is complete!

To Do List

Things left to handle:

  • When deployment completes, restart the nginx docker container. This should help with any issues of changing the contents of the shared volume containing the static site.
  • Consider different deployment workflows. Merging to main is quick and easy, but since I can trigger actions off merged pull requests and considering it would be nice to have one Actions workflow for production and a second Actions workflow for a staging/test environment, I might tweak this setup. New posts would go into a separate branch, and pushes to that branch would trigger the staging workflow to run and deploy to something running locally on my network which I could use to validate the site before merging that branch to main for live deployment.
  • Get a publishing workflow from iOS working. My intial choice is using the excellent WorkingCopy App, which allows me a simple text editor and the ability to merge directly to main. Even if I go the staging/prod split workflow route, I should still be able to manage PR’s from that app.

Wrap Up

Overall, this whole thing took me less than a day to setup. GitHub Actions is pretty powerful and the learning curve is pretty shallow, which is great for folks like me who are simply enjoying a fun hobby.

Thanks for reading!