Hugo: Self hosted?
I started playing around with Hugo, a static site generator based on the Go language, a couple of years ago when I first started messing around with creating blog sites. Hugo is really versatile, and being able to build websites with variables and logic is a lot of fun.
Typically, since the projects I work on are small and don’t get a ton of traffic I can get away with hosting on something like GitHub Pages or Netlify. What really nice about this is how easy it is to link up your GitHub repository and have a website live in a matter of moments. You can even set up your static site generator to trigger a new build any time your repository receives updates.
However, doing things the easy way just doesn’t come naturally to some of us. I have access to a decent little server and a few dormant single-board computers, so naturally I want to self-host whatever I can.
With Hugo, Git, and a bit of magic we can easily host our static files for completely free without having to rely on another service like GitHub or paying for a VPS.
The Problem
If you were to do a DuckDuckGo search for something like “automating Hugo deployments” you get all kinds of results over the last few years offering all kinds of various solutions including git hooks, GitHub Webhooks, GitHub Actions, and so on. The problems that I kept running in to are
- I want to self-host and not do things in the cloud
- I want to host my own repositories instead of relying on GitHub
- I’m mostly a newb to programming
- There was not a perfect plug-and-play solution that fit all my needs.
It took me quite a bit of research, and a good day of troubleshooting and debugging things before I finally figured out how to get my automatic Hugo deployments working the way I wanted. Typically, the workflow for creating content for Hugo is something like…
- Create a new markdown file with content
- Commit and push the changes
- Run the
hugo
command to build the site - SCP, FTP, or otherwise find a way to transfer the updated site files to the hosting server
- Replace the existing site files with the new.
With a little git hook magic we can completely cut out steps 3-5, turning our deployment process into nothing more than issuing a git push
command.
TL;DR: Automated Hugo Deployments
A lot of searching went into this very simple solution. Most of it was inspired by noelboss on GitHub and his post-receive script, and Sean Todd’s very similar blog post explaining how he went about this setup. However, I had to make some modifications to this script in order for it to fit my needs.
The prerequisites for this are pretty simple. You just need:
- SSH access to your public web server
- Git installed on both your local and remote systems (This guide from Karl Broman is great)
- Git post-receive script to upload to your server.
Once we have everything setup and ready, lets take a look at the post-receive script and break down what it is doing.
I’ve added comments to the script whenever possible to try to make it as easy to understand as possible. The few lines we want to focus on are lines 11-15:
11 TARGET="/path/to/deploy-folder" # Deployment location - Where Hugo should put the /public/ contents after build
12 BUILD="/path/to/build-folder" # Build location - Where the repo will be copied, and the source folder for Hugo to build
13 PUBLIC="/var/www/html/website.com" # Public location - Where you serve your http content `/var/www/html/PUBLIC`
14 GIT_DIR="/path/to/your/repo.git" # Git Repository location - bare git folder - `git init --bare /path/to/GIT_DIR`
15 BRANCH="main" # Only pushes to this branch will deploy
These lines are defining the variables used by the script, and should really be the only lines that need to change. Let’s break down what each of these variables are doing:
TARGET="/path/to/deploy-folder"
sets the target deployment folder, or where Hugo will output the static web files that are usually placed in the /public/ folder inside the Hugo working directory.
BUILD="/path/to/build-folder"
sets the build location. This is where Git will “checkout” the latest changes - the Hugo working directory. This is the source folder for running Hugo’s build.
PUBLIC="/var/www/html/website.com"
is the publicly accessible folder that your web server is pointing to. The script will symlink this folder to the TARGET variable.
GIT_DIR="/path/to/your/website-repo.git"
sets the location of the bare git repository we need to set up. This folder holds the information for your repository, and is not the same as the working folder used for the BUILD variable.
BRANCH="main"
is the branch that we want to be deployed. The script will check that changes were pushed to this branch, and exit without doing anything if any other branch is used.
Now that we have our script and understand which variables we need to pay attention to, we’re ready to put everything into place. We start off by connecting to our web server and preparing our directories for our TARGET, BUILD, and GIT_DIR folders. I simply put these in my $home
folder.
mkdir ~/deploy_folder
mkdir ~/build_folder
And finally, create a bare git repo:
git init --bare website-repo.git
After creating these folders, we’ll need to make sure to set the variables with the full path so that the script will run properly. The changes will look similar to this:
TARGET="/home/user/deploy-folder"
BUILD="/home/user/build-folder"
GIT_DIR="/home/user/website-repo.git"
With everything into place, we’ll want to download/clone/upload the post-receive script so that it will run whenever we push changes to our new bare git repo. I find it easiest to clone the GitHub Gist to a new folder:
git clone https://gist.github.com/8dff02f2f40b9fb37670df8b06bc4aae.git ~/post-receive.script
and copy the post-receive.sample file from there to my bare git repo:
cp ~/post-receive.script/post-receive.sample ~/website-repo.git/hooks/post-receive
Long story short, move this script into the hooks/
directory inside our bare git repo and rename it to remove the .sample
extension so that git will recognize it. Then set the script as executable so our system can run it:
chmod +x ~/website-repo.git/hooks/post-receive
Using the Script
This is the fun part! All that’s left is adding a –bare repository as a remote for our local working copy. With SSH key access in place, all that’s necessary is navigating to the working directory:
cd /path/to/hugo_working_directory
and add the new remote repository:
git remote add production user@web.server.com:path/to/your/website-repo.git
# Change "production" to whatever you'd like this repo to be called
Let’s check that the remote is added properly and ready to receive our push by using the commands:
git remote -v #To show the remotes we have
git remote show production #To give the stats of our remote called "production"
If the remote is added and everything else looks right, then we should be ready to go! All that’s left is to push the Hugo site to the newly added remote repository and watch the magic happen:
$ git push production master
Enumerating objects: 21, done.
Counting objects: 100% (21/21), done.
Delta compression using up to 12 threads
Compressing objects: 100% (11/11), done.
Writing objects: 100% (12/12), 1.32 KiB | 337.00 KiB/s, done.
Total 12 (delta 7), reused 0 (delta 0), pack-reused 0
remote:
remote: /===============================
remote: | Push received ... Beginning Build and Deployment script ...
remote: | by d00vy
remote: |
remote: | Ref: refs/heads/master received. Deploying master branch to production...
remote: Already on 'master'
remote: | Building with Hugo from /home/doovy/build-folder to /home/doovy/deploy-folder...
remote: Running Hugo Static Site Generator v0.78.2-959724F0/extended linux/amd64 BuildDate: 2020-11-13T10:16:23Z
remote: INFO 2020/12/01 16:57:14 Using config file:
remote: Start building sites …
remote: INFO 2020/12/01 16:57:14 syncing static files to /home/doovy/deploy-folder/
remote:
remote: | EN
remote: -------------------+-----
remote: Pages | 16
remote: Paginator pages | 0
remote: Non-page files | 0
remote: Static files | 37
remote: Processed images | 0
remote: Aliases | 5
remote: Sitemaps | 1
remote: Cleaned | 0
remote:
remote: Total in 143 ms
remote: | Symlinking /home/doovy/deploy-folder to /var/www/html/d00vy.com
remote: ln: failed to create symbolic link '/var/www/html/d00vy.com/deploy-folder': File exists
To 192.168.11.103:~/d00vy.com.git
7ee59f2..4e880b4 master -> master
* Notice that the symlink at the end of the script fails. This is because I've already run this script and the symlink has already been made. The error doesn't really make any difference, and at this point, mostly acts as confirmation that the symlink is set correctly.
Conclusion
My Hugo development workflow is definitely a lot simpler now, and everything with this website is now self-hosted! Instead of spending more time moving things around and deploying my website, everything is automated and I can focus more on my content.
Self-hosting my services and automating monotonous processes like these are always rewarding. Hopefully you find something useful in this guide, and feel free to leave comments or contact me if you need!