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 service.


This is what I’m using now. It includes just the build-specific bits, then delegates to a script (described below):

image: alpine/edge
  - <your ~/.aws/config secret UUID>
  - aws-cli
  - hugo
  - <https git URL of the repo you're deploying>
  - deploy: |
      cd <name of repo>

NOTE: This will deploy from any branch that you push. If you want to only deploy when you push main, you can add a check-branch action that aborts early, as suggested here.

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.

You don’t actually need the sources section when you’re deploying via a .build.yml – the repo containing the .build.yml file is automatically added. But it’s very convenient for testing by submitting ad-hoc build files via sourcehut’s web interface.


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.

set -u
set -e
profile=<your profile name>
bucket=s3://<your s3 bucket name>
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..."

echo "Deploying..."
echo "Synchronizing directory $PWD/$dir"
aws --profile="$profile" \
  s3 sync "$dir" "$bucket" \
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 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.


This one is simple. It generates the contents of the website, and is just hugo plus whatever flags you decide to use:

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 (for instance, adding a minifying operation) and inspect the output in the public directory without deploying; you can make changes to and deploy manually, from your local machine, before trying the automated version in .build.yml.

Real examples


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