Automatically deploying a Hugo static website to S3 via sourcehut
This is how I’m auto-deploying a static website generated with Hugo, served from Amazon S3, with a CloudFront frontend (for HTTPS, mostly). The code is stored in sourcehut, and automatically deployed using sourcehut’s builds.sr.ht service.
.build.yml
This is what I’m using now. It includes just the build-specific bits, then
delegates to a deploy.sh script (described below):
image: alpine/edge
secrets:
- e543f154-cfa9-4b56-b8aa-adbd8ec0adf7
packages:
- aws-cli
- hugo
environment:
REPO: blog.michaelkelly.org
tasks:
- deploy: |
cd ~/"${REPO}"
./scripts/deploy.sh
submitter:
git.sr.ht:
enabled: true
allow-refs:
- refs/heads/main
Here’s the build manifest reference.
2026 UPDATE: I’ve added a submitter section to control which branches get
pushed. This means when you push to non-main branches, you don’t deploy –
this is probably what you want.)
NOTE: You can add a sources
section to the file as well, which is not
required normally, but is very useful for testing by submitting ad-hoc build
files via sourcehut’s web interface.
To authenticate to the AWS API, we use a build
secret containing an entire
~/.aws.config file. This keeps the build file nice and simple.
scripts/deploy.sh
I use variations of this script for different repos, so it’s designed somewhat parametrized. To adapt to a new repo you only need to adjust the variables at the top.
Note that I use a non-default named
profile
with the --profile flag to aws. This is not necessary; it’s just my
preference. You can set it to default.
#!/bin/bash
set -u
set -e
cfg=$HOME/.aws/config
profile=<your profile name>
bucket=s3://<your s3 bucket name>
dir=public
cf_id=<your cloudfront ID>
scripts=$(dirname $0)
which aws || (echo "'aws' command not found. Aborting."; exit 2)
[ -f $cfg ] || (echo "Config file $cfg does not exist. Aborting."; exit 2)
echo "Building..."
${scripts}/generate.sh
echo "Deploying..."
echo "Synchronizing directory $PWD/$dir"
aws --profile="$profile" \
s3 sync "$dir" "$bucket" \
--cache-control=max-age=3600
echo "Invalidating CloudFront..."
aws --profile=admin \
cloudfront create-invalidation \
--distribution-id "${cf_id}" \
--paths "/*"
Note that you don’t need to generate the static site content beforehand – this
script runs generate.sh automatically. This prevents you from accidentally
deploying outdated generated content.
There’s a lot of room for improvement around CloudFront here – this script wastefully invalidates everything every time you deploy. It works for me because my sites are relatively small and infrequently updated.
scripts/generate.sh
This one is simple. It generates the contents of the website, and is just
hugo plus whatever flags you decide to use:
#!/bin/bash
hugo --minify --cleanDestinationDir -d ./public || exit 1
By splitting everything apart like this, you can easily test and use each
component individually: You can make changes to generate.sh (for instance,
adding a minifying operation) and inspect the output in the public directory
without deploying; you can make changes to deploy.sh and deploy manually,
from your local machine, before trying the automated version in .build.yml.
Real examples
For blog.michaelkelly.org:
- Here’s the
.build.ymlfile. - Here’s the
scriptsdirectory.
I hope this is useful to someone, if only to myself, in the future.
Next Post: LXC Containers on Debian, Part 1 (Setup)
Previous Post: Mirroring sourcehut repositories to GitHub