GitHub Actions & rsync based deployments for Hugo
Let’s start with the full GitHub Actions workflow file and then work our way through it step by step. You can also view the file on GitHub as a gist. This will serve as documentation for myself as well (the next time I’ll need it).
1name: Build & Deploy
2on:
3 push:
4 branches: [ main ]
5 workflow_dispatch:
6
7jobs:
8 deploy:
9 runs-on: ubuntu-latest
10 concurrency:
11 group: ${{ github.workflow }}-${{ github.ref }}
12 cancel-in-progress: true
13
14 steps:
15 - uses: actions/checkout@v7
16 with:
17 submodules: recursive
18 fetch-depth: 1
19 lfs: false
20
21 - name: Setup Hugo
22 uses: peaceiris/actions-hugo@v3
23 with:
24 hugo-version: '0.169.0'
25
26 - name: Build
27 run: hugo --minify
28
29 - name: Deploy
30 uses: burnett01/rsync-deployments@v9
31 with:
32 switches: -avzr --delete
33 path: public/
34 remote_path: ${{ secrets.REMOTE_PATH }}
35 remote_host: ${{ secrets.REMOTE_HOST }}
36 remote_user: ${{ secrets.REMOTE_USER }}
37 remote_key: ${{ secrets.REMOTE_PRIVATE_KEY }}
The Setup
Pretty basic setup at the top. We have on push to main branch which means that every time there’s a new commit added to the main branch this action is activated, and this includes merges to the main branch or direct pushes to it.
I personally like to add workflow_dispatch: to my workflows because it allows you to manually trigger the workflow in GitHub. I’ve only used it once so far for this project (and that to test if it works), but there are cases where it is quite handy to have it. Otherwise you’ll have to create empty (or test) commits just to trigger the workflow.
Runs On & Concurrency
As this is quite a small project that has very few moving parts and in general very well maintained and popular software I’m using ubuntu-latest. Which means that it uses whatever the latest version of Ubuntu (as deemed by GitHub) is. Over time the version used gets updated to, in fact, it has just recently changed to Ubuntu 26.
In bigger and more important projects you’d want to pin this to a specific version to make sure that it runs as expected. And then over time review changing the pinned version so that you aren’t running an end of life version that no longer receives updates.
The concurrency is something that is a bit much for this project, but it’s a good practice to have it, and a good thing to learn about.
With the above configuration it allows for in progress workflows in the same branch to be cancelled should a newer one be started. That is, no point in finishing an old run if there’s already a newer one starting, but do this only for the specific workflow and specific branch combination.
Step: Checkout
Submodules are needed in this case because I’m using a Hugo theme, and those are typically added as a git submodule to a project. Recursive in theory because the theme could be using submodules as well, so just making sure that we don’t break that.
The fetch-depth and lfs are actually both using the default values, so you don’t need to have them as such, but I like to be explicit with things like this. fetch-depth: 1 will only fetch the specific commit from the git repository, this is very helpful for performance as your repository grows in size. While lfs specifically says do not download git LFS (Large File Storage) files, which are files that are larger than 50 MiB and require special setup to be added to GitHub’s git repositories.
Steps: Setup Hugo & Build
These are pretty barebone steps, the only options I’ve added are pinning the Hugo version to the same version that I run locally currently, and during the build process to minify the output.
Step: Deploy
The step the requires most work (and most testing) is the step that takes the Hugo generated content and uploads it with rsync to my server. In order for this step to work you’ll need to (or at least want to) setup a new SSH Key to be used purely for these deployments so that you can revoke it in case of security needs.
There’s good documentation and troubleshooting guides available for this step in the git repository of the burnett01/rsync-deployments action.
You will also need to setup the needed secrets in your repository settings. You can learn more about them in the Using secrets in GitHub Actions article in GitHub Docs.
Important, and I can’t stress this enough: you will definitely want to run this workflow a few times first with -avzr --delete --dry-run switches (note the --dry-run part), just to make sure it doesn’t delete things it shouldn’t. Yes, it should have the delete flag because otherwise you’ll end up with content that shouldn’t be there anymore. So run it a few times, check the run summary and debug output, and once you are happy and brave enough, remove the --dry-run from the switches to actually deploy the site.
Future improvements and general thoughts
While I’m quite happy with how it works currently, there’s always room for improvement and changes. Some thoughts that you (or I) might want to change about this:
- Change
ubuntu-latestto a pinned version and the same goes with the individual actions in the steps. Pin them to a specific commit SHA and add a comment to indicate which version that is.- For example:
uses: burnett01/rsync-deployments@c830011 # v9.0.3 - This makes it very explicit which versions should be used for the workflow
- For example:
- Figure out a safer(?) way of doing the deploy step. Rsync and deleting files works perfectly fine, but is there maybe something better out there? Maybe something made by/for Hetzner? But that would limit it to Hetzner, which isn’t something I’d like to do
Help & Troubleshooting
Should you have any questions, or feedback, or be in need of help, feel free to contact me.