Publishing a website on Fastmail - Part III

Posted on Jan 12, 2026

After many failed deploys and at least two existential crises, I went back to first principles: simplify.

No modules. No rclone. No trying to obscure passwords mid-script.

Just a GitHub Actions workflow that:

  • Checks out the repo
  • Installs Hugo
  • Downloads the Ananke theme manually
  • Builds the site
  • Uses curl and MKCOL to recursively ensure folders exist
  • Uploads files one-by-one to Fastmail via WebDAV

You’ll need an app-specific password from Fastmail to then add that to the secrets of your repo, but that is the only semi-complicated part of this process.

The Final Working Workflow

name: Build and Upload Hugo Site to Fastmail

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    env:
      FASTMAIL_USERNAME: ${{ secrets.FASTMAIL_USERNAME }}
      FASTMAIL_PASSWORD: ${{ secrets.FASTMAIL_PASSWORD }}
      REMOTE_DIR: "Sites/blog"
      LOCAL_DIR: "public"

    steps:
      - name: Checkout repository (with submodules)
        uses: actions/checkout@v3
        with:
          submodules: true

      - name: Set up Hugo
        uses: peaceiris/actions-hugo@v2
        with:
          hugo-version: 'latest'
          extended: true

      - name: Download Ananke theme
        run: |
          mkdir -p themes
          git clone --depth=1 https://github.com/theNewDynamic/gohugo-theme-ananke.git themes/ananke

      - name: Build Hugo site
        run: hugo --minify

      - name: Upload Hugo site to Fastmail (via curl + WebDAV)
        run: |
          set -euo pipefail
          BASE_URL="https://webdav.fastmail.com"
          TARGET_URL="${BASE_URL}/${REMOTE_DIR}"

          echo "Uploading from local: $LOCAL_DIR"
          echo "To Fastmail folder: $REMOTE_DIR"

          find "$LOCAL_DIR" -type f | while read -r FILE; do
            REL_PATH="${FILE#$LOCAL_DIR/}"
            DEST_URL="${TARGET_URL}/${REL_PATH}"

            # Ensure directories exist on the server
            IFS='/' read -ra PARTS <<< "$(dirname "$REL_PATH")"
            CURRENT_URL="$TARGET_URL"
            for PART in "${PARTS[@]}"; do
              [[ -z "$PART" ]] && continue
              CURRENT_URL="$CURRENT_URL/$PART"
              curl -s -o /dev/null -u "$FASTMAIL_USERNAME:$FASTMAIL_PASSWORD" -X MKCOL "$CURRENT_URL" || true
            done

            # Upload the file
            curl -T "$FILE" -u "$FASTMAIL_USERNAME:$FASTMAIL_PASSWORD" "$DEST_URL"
            echo "Uploaded: $REL_PATH"
          done

The Result

Every time I commit, GitHub Actions builds the site, and Fastmail hosts it. Static, fast, and mercifully free of anything that needs maintenance.

My blog now lives at:

https://blog.halleyadams.uk

It took too long. It broke in more ways than I can count. But it works. And I never had to install Hugo locally.

That’s good enough for me.

Up next: How to point a telescope at a sandwich.