DevBlog: Hugo and Gitea Actions
I wanted to have a small blog where I could document my experiments and conclusions, so that they could be tracked for someone else’s future use. My blog is running on a Raspberry Pi 4 Model B Rev 1.2 at home where I manage all the services through YunoHost, so having a static site generator seemed like the best choice for that.
The kind of features I wanted were:
- I can edit and preview posts I’m working on, possibly more than one in parallel
- Basic editing tools: formatted text, images, code highlight
- Basic theming support
- I can save the drafts I’m working on and keep working on them on different computers
- Publish the posts on the website once I’m ready.
This is all kinds of things that, e.g., Wordpress do, but that definitely has a different impact on performances, especially on such a small server like mine. Making it work with a static site generator was another thing.
Tools
Hugo is one of the main static site generators used nowadays. It’s small, fast, and quite flexible. It uses Markdown, a simple syntax that allows basic formatting: headers, bold, italic, lists, anchors, and so on.
Gitea is an open source, lightweight DevOps platform. Until recently the most you could do was to have a git repository and track tasks and bugs, but since version 1.19 the team behind added Gitea Actions, a mechanism very similar to GitHub Actions. With it, a number of operations can be automated.
These two tools were enough together to satisfy all my requirements!
YunoHost setup
To create my devblog site I used the standard My Webapp application, without any MySql or PHP support (the site is static after all). One interesting thing is that the default settings already setup the ability to copy files using SFTP, which is very good for our later integration with Gitea Actions. Because of this, though, I needed to generate SSH private/public keys.
In order to do this, I had to perform a series of step in the directory of the webapp (in my case, /var/www/my_webapp
) as the webapp user (in my case, my_webapp
). Before this, I had to give permissions to the directory that allowed me to change the contents:
$ chown my_webapp:www-data /var/www/my_webapp
And then:
- create the ssh directory structure
$ mkdir .ssh $ chmod 700 .ssh
- create the ssh keys (this would actually be better done on another system for security reasons):
$ ssh-keygen -t ed25519 $ cp .ssh/id_ed25519.pub .ssh/authorized_keys $ chmod 600 .ssh/authorized_keys $ chmod 600 .ssh
Afterwards, set it back to being owned by root (otherwise the chroot mechanism of sftp won’t allow any connection):
$ chown root:root /var/www/my_webapp
Now we have to save the ssh private key (.ssh/id_es25519
) for the Gitea Actions setup, and also the output of ssh-keyscan for the sftp server (in my case: blog.foxthesystem.space):
ssh-keyscan *url of the sftp server*
This will output the ssh fingerprint of the server, and it will be used to avoid sftp to request confirmation for the connection.
Hugo setup
Hugo doesn’t require any special setup: it’s enough to follow the official documentation. In my case I also transformed the site into a Hugo module to express dependencies on the theme without meddling with git submodules.
Gitea setup
The initial setup for Gitea Actions is outlined in the documentation: enable actions on the server, on the project, and install a runner on a machine with Docker (or equivalent).
The main part of the setup is defined by the workflow itself. This will require to first setup a couple of secrets.
Secrets are a system used to save sensitive data (like ssh keys or passwords) to be used during the workflow without having them public on the server. The ssh private key and server fingerprint can be saved there. To do that, I add their contents to the repository’s secrets:
Once this is done, there’s the workflow file (whose latest version you can find here):
name: Update
run-name: Update devblog
on:
push:
branches:
- main
jobs:
build-and-update:
runs-on: ubuntu-latest
steps:
- name: Install Hugo and Go
run: |
mkdir "$RUNNER_TEMP/hugo"
cd "$RUNNER_TEMP/hugo"
wget https://github.com/gohugoio/hugo/releases/download/v0.117.0/hugo_0.117.0_Linux-64bit.tar.gz
tar -xzvf hugo_0.117.0_Linux-64bit.tar.gz
mkdir "$RUNNER_TEMP/go"
cd "$RUNNER_TEMP/go"
wget https://go.dev/dl/go1.21.0.linux-amd64.tar.gz
tar -xzvf go1.21.0.linux-amd64.tar.gz
- name: Check out repository code
uses: actions/checkout@v3
- name: Compile site
run: |
cd ${{ gitea.workspace }}
export PATH="$PATH:$RUNNER_TEMP/go/go/bin"
"$RUNNER_TEMP/hugo/hugo" mod get
"$RUNNER_TEMP/hugo/hugo"
ls public
- name: Upload site
run: |
mkdir ~/.ssh
chmod 600 ~/.ssh
echo "$SSH_KEYSCAN" > ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
cd ${{ gitea.workspace }}/public
sftp my_webapp__2@devblog.foxthesystem.space <<EOT
put -R . www
exit
EOT
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_KEYSCAN: ${{ secrets.SSH_KEYSCAN }}
The first job it performs is to install Hugo: packaged versions are usually old, so I download a specific binary version and upload it in a temp directory (using the environment variable RUNNER_TEMP
).
Then it checks out the code, using the standard checkout
action. Two points of interest here:
- This action won’t work if your Gitea server is installed in a subdirectory of your server (e.g.,
https://yourserver.site/gitea
). Because of this I had to move Gitea on its own subdomain (e.g.:https://gitea.yourserver.site
). The operation, btw, wasn’t painless, and I had to completely remove Gitea and then reinstall it, so… well, beware! - I wasn’t able to make it work on a private repository, but I guess it’s possible - this proposal should track how things currently work and improvements about it
The code compilation is relatively straightforward: mod dependencies are downloaded, and then the hugo command is run to compile the whole code.
Finally, the hard part: uploading the site. It must be considered that the compilation of the code happens on a runner, a different machine from the server, and that potentially the gitea server, the runner and the web server are on different hosts. The whole section is a way to setup an .ssh directory with the private key (extracted from the secrets) and preloaded ssh fingerprints (also extracted from the secrets), and then run an sftp command to copy the compiled directory to the server.
This whole workflow is run only when code is pushed to main.
The final workflow
My blog workflow is thus following:
- Have a clone of the devblog repository on every machine where I want to edit it
- When I want to create a new post:
- I pull the current version of the blog to main
- Create a branch for the draft
- Write the draft, committing and pushing as many times as I like (this will not trigger the workflow, since we’re not on main)
- While writing the draft, I can keep a hugo server running to see the final result
- Once the draft is completed, update the front matter of the post setting
draft
tofalse
- squash-merge the branch into main (
git checkout main
,git merge --squash branch-name
) - This will trigger the workflow which, in a matter of seconds, will update the static version of the blog, without any manual intervention.
- delete the old branch post.
And that’s what I’ve just done with this post too!
Conclusions
The whole setup required quite a lot of different parts: configuring YunoHost, Gitea, Gitea Actions, Hugo, SSH. Way too much work for a simple, static blog, but the end result is that I have full control of the whole chain, and everything is self-hosted and self-running, without relying on any external service.
This is definitely not a setup that it’s useful for anyone, also because there are even more considerations to take (on which storage is saved everything? Is the storage redundant? Has the storage a backup? Are the disks sensitive to many write operations?), but it’s definitely interesting how nowadays even a complex setup like this one can be mostly automated and self-managed.
If there’s something that is unclear or missing in this post, or if you try or have tried out similar setups I’d love to hear from you on Mastodon (see the social links on the side!).