How to Set Up CI/CD for a Hugo Project
Introduction
In this post I’ll explain how to set up CI/CD for a Hugo project that uses GitHub for version control and is hosted on a Digital Ocean droplet. I’ll assume you already have a repo and droplet set up on those platforms and that you have an existing site that you can visit.
The plan is that we’ll have GitHub spin up a VM via a GitHub Action, connect to our droplet via SSH, and run the necessary commands to pull down the latest changes and rebuild the site.
We’ll need two sets of SSH keys. One set will be for your droplet to connect to GitHub and pull down a repo. The other will be for GitHub to connect to your droplet and run commands.
Prerequisites
Please follow GitHub’s guide on how to set up SSH authentication and pull down a repo.
Ensure your key is automatically added to the SSH agent when you login to the droplet by adding the identity to your ~/.ssh/config
.
Host github.com
User git
IdentityFile ~/.ssh/<your key>
SSH for CI/CD
Generating the keys
Login to your droplet.
Navigate to
~/.ssh
.Generate a public and private key pair with:
ssh-keygen -t ed25519
-t
specifies the encryption algorithm to use. It doesn’t matter too much, but I prefer the newer EdDSA (Edwards-curve Digital Signature Algorithm), and GitHub supports it.When prompted enter a file name:
Enter file in which to save the key (/home/user/.ssh/id_ed25519):
The files will be called
name
andname.pub
for the private and public keys, respectively.One quirk about
ssh-keygen
here. If you use the default name, your keys will appear in~/.ssh
by default. If you provide your own name, as we just did, they will appear in the current working directory (cwd) instead. That’s why your cwd should be~/.ssh
.Add
<name>.pub
to your authorized_keys file:cat <name>.pub >> authorized_keys
>>
means we want to append toauthorized_keys
on a new line.
Secret variables
We’ll need to add some secret variables that our GitHub Actions can use.
Show the contents of the private key you just generated in the terminal.
cat ~/.ssh/<name>
Copy the contents to your clipboard. Ensure there are no weird newlines occuring due to a narrow terminal when you copy.
Go to your repo on GitHub.
Click the Settings tab > Secrets and variables > Actions.
Click “New repository secret”. Repeat for the following three secrets:
- PRIVATE_KEY = content of the private key
- SERVER_IP = IP of your droplet
- SERVER_USER = the user you want to login to your droplet with
Now that the private key is in a secret variable on GitHub, you should delete the one that you generated on the droplet. A private key should generally only exist in one place so there is less of a chance it is compromised.
Setting up GitHub Actions
Now we can move on to automating our deployments. We’ll add a simple action to start and build on it.
Add .github/workflows/deploy.yaml
to the root of your repo with the following content:
name: Deploy
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: echo
run: |
echo "It's just too easy."
This creates an action named Deploy
that will trigger whenever we push to the main
branch. GitHub will spin up a VM with the latest Ubuntu version and echo a message to the terminal. GitHub will also keep a history of these actions.
To see the outcome of the action:
- Push the changes to your main branch.
- Click the Actions tab of your repo.
- Click your
Deploy
action on the left. - Click your latest commit.
- Click the deploy job.
- Click the
echo
step and observe the message.
Let’s update with the necessary code to make the SSH connection.
name: Deploy
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: create private key
run: |
mkdir ~/.ssh
touch ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
echo "${{secrets.PRIVATE_KEY}}" >> ~/.ssh/id_ed25519
- name: create known hosts
run: |
touch ~/.ssh/known_hosts
ssh-keyscan ${{secrets.SERVER_IP}} >> ~/.ssh/known_hosts
- name: start agent
run: |
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
- name: ssh
run: |
ssh -T ${{secrets.SERVER_USER}}@${{secrets.SERVER_IP}}
- name: exit
run: |
exit
Key takeaways:
- We change permissions of our private key file to 600. The default is 644, but OpenSSH considers that too permissive and would ignore the key.
- The
known_hosts
file ties hosts (IPs or host names) to public keys.ssh-keyscan
helps to generate that for us. - The
-T
option for thessh
command disables pseudo-terminal allocation. Usually, SSH connects the host machine to our terminal so we can input commands. In this case, we want to send commands without an interactive terminal. If we didn’t specify-T
, we could still connect, but we’d get this message: “Pseudo-terminal will not be allocated because stdin is not a terminal.”
Pulling the repo and building our Hugo site
For this step, I’ll assume you already have a site configured and that you’re serving the public
folder that Hugo generates. I’ll also assume that you can pull down GitHub repos to your droplet as soon as you login without adding the key to the SSH agent.
Make these modifications to the ssh
step:
- name: ssh
run: |
ssh -T ${{secrets.SERVER_USER}}@${{secrets.SERVER_IP}} << EOF
cd /root/of/your/hugo/project
git pull
rm -rf public
hugo --gc --minify
EOF
This is called a heredoc (here document). The first token after <<
is where we specify a delimiter. The remote machine will execute the commands until it reaches EOF
. If your build process will involve something more complex, you could create a script on the droplet and execute the script via SSH.
I like to recreate the public
folder each time, but that is optional.
Hugo arguments used:
--gc
tells Hugo to remove unused cache files after the build.--minify
removes white space from the outputted files so that the file size is smaller.
If you push to your GitHub repo now, it should kick off the action and update your site!
Conclusion
Now you can add new posts or make quick edits to your Hugo site and deploy them with ease.
Support the blog! Buy a t-shirt or a mug!