RSSAmplifier

BurgeonLab: Full-text · Aug 13, 2025

GitHub Pages Alternative: SourceHut Builds + Bunny.net

0
Sign in to vote or save

Naty S · BurgeonLab

Learn how to replace GitHub Pages with SourceHut Builds, Bunny Storage and Bunny CDN. Set up a CI static site deployment workflow replacing GitHub Actions, with tools like SFTP, Rclone, and Curl using SourceHut's CI platform running on Alpine Linux.

This post was last updated 1 year ago. The core ideas should still be useful, but tech moves fast; always check the latest docs for current best practices before applying what is mentioned.

Finding a GitHub Alternative

I remember the first time using GitHub and feeling slightly intimidated by the complexity; but at the same time, also intrigued by all the cool projects I found there. This was before Microsoft bought GitHub. I only started delving deeper into the platform when I started using static site generators in 2023 or so, trying out their free hosting offered with GitHub Pages.

Recently, I’ve become more comfortable using GitHub (likely because I learnt more about Git versioning). I even managed to contribute to a few FOSS projects last month with pull requests, which was a personally delightful moment! But just as I found familiarity, I’m leaving it as my web host for this blog (using GitHub Pages & GitHub Actions) and for hosting my Git repositories and blog source code.

For someone who cares about:

  • open-source community
  • online privacy
  • dodging vendor lock-in with Big Tech
  • avoiding AI infusing into every possible nook and cranny
  • and breaking free from the “enshittification” cycle;

I did my research on GitHub alternatives and decided to migrate to SourceHut (AKA sr.ht).

What is SourceHut

SourceHut is a collection of development tools presented on a minimalist platform, currently in alpha, that is focused on and prioritizes the open source community.

From SourceHut’s home page:

  • Absolutely no tracking or advertising
  • All features work without JavaScript
  • No AI features whatsoever
  • Many features work without an account
  • 100% free and open source software
  • Supports hundreds of free software projects

The GitHub alternatives that made my shortlist and suited my needs included Codeberg (which doesn’t seem to support hosting private code repos that aren’t open licenced), and self-hosting Forgejo / Gitea. GitLab is a no go for those in Hong Kong because they have licensed the brand to JiHu, a Chinese company that I’m not familiar with. Migrating to these platforms would have been easier as their look and feel are more similar to GitHub..

But I decided to use SourceHut because:

  1. Their manifesto and the creator behind SourceHut (Drew Devault) seems very committed to the open source community
  2. They are based in Europe
  3. I ended up liking their minimal, “nerdy looking”, text-based, no frills user interface after browsing around
  4. It’s snappy
  5. Features are modular and lightweight
  6. Yay, no AI!

Features of SourceHut—The Hacker’s Forge

SourceHut’s features are all modular and almost all are free during their alpha stage. They have made it clear payment will be required from beta onwards. The feature I need to replace GitHub Pages with is their continuous integration (CI/CD) platform called Builds (builds.sr.ht). It is one of two features (the other being chat.sr.ht, a hosted IRC bouncer service) that requires a SourceHut subscription. I paid for the Amateur hacker tier for $20 USD annually. If you have the means to self-host, SourceHut also supports that—but I’m quite happy to support this platform.

Screenshot of BurgeonLab.com Hugo source code on SourceHut's Git hosted repository.

Minimal looking Git repo

Tip

One cool feature of SourceHut’s Builds is the ability to SSH into the build’s virtual environment within a short window (10 minutes) if a build fails. Handy for troubleshooting!

SourceHut’s Disadvantages

A few downsides relevant to my use-case are that the site feels a bit rough around the edges, especially the documentation. I read from the blog, there has been some financial troubles and a big DDOS that brought everything to a halt in early 2024. However, I hope that the infrastructure improves and that the platform gains popularity in the near future. I’ve noticed that it’s more difficult to discover and save FOSS projects on SourceHut than say on GitHub with its star and lists system. If you like the social aspect of GitHub, you might not like the simplicity or “lack of features” of SourceHut (but this point can go both ways).

Therefore, do some research before switching to SourceHut, as it is indeed quite different to the alternatives—especially regarding its contribution model, which is email-based with a patch workflow instead of the more common GitHub-type pull request workflow. I think for most people, Codeberg would be a better bet.

Here are some pages that talk about SourceHut I found insightful:

Step-by-step Migration Guide: Hugo Static Site CI Deployment

Here is a closer look at the practical aspects of replacing GitHub to two different services in one move! I made an overview diagram1 showing the drop-in replacements for GitHub’s services.

---
title: GitHub Replacement Workflow
---
flowchart TD
  GitHub["GitHub"] -.-x A["Git repo"] & B["GitHub Actions (CI/CD)"] & C["GitHub Pages (free hosting)"]
  A --> D["git.sr.ht"]
  B --> E["builds.sr.ht (CI/CD)"]
  C --> F["Bunny.net Storage (budget-friendly hosting)"]
  D --> G["Git repo (private) with Hugo source code"]
  G --> E
  E -->|builds.yml| H["Hugo Build & Deploy public folder to Bunny"]
  H -->|via rclone and SFTP| F
  F -->|via Bunny.net CDN| I["Static Hugo blog is up and running 🎉"]
  style E stroke:#FFB800,fill:#ffe39c
  style D stroke:#FFB800,fill:#ffe39c
  style F stroke:#FFB800,fill:#ffe39c
  style G stroke:#FFB800,fill:#ffe39c
  style H stroke:#FFB800,fill:#ffe39c
  style A stroke:#E64814,fill:#ffa284
  style B stroke:#E64814,fill:#ffa284
  style C stroke:#E64814,fill:#ffa284
  style GitHub stroke:#E64814,fill:#ffa284
  style I fill:#ffffff,stroke:#a1a1a1
  %% Other styles
  linkStyle default stroke:#FFB800,stroke-width:2px

Step 1: Transfer Git Repository From Github to SourceHut

  1. Register for a free SourceHut account. If you’re using SourceHut with their Builds service, you will have to subscribe to one of the tiers (they all offer the same services). But if you are just using their hosted git repository service, you don’t have to pay.

  2. Add your device’s .pub SSH key to your account.

  3. Create your first Git repository on git.sr.ht. For private repositories, copy the read/write clone URL for the next step (e.g. git@git.sr.ht:~username/your_blog). Set default branch to master (some prefer main instead).

  4. Go to your local Hugo directory (or other SSGs like Jekyll, 11ty) and remove the old Git origin, replacing it with the new SourceHut origin, and confirm the remote is set correctly:

    1git remote remove origin
    2git remote add origin git@git.sr.ht:~username/your_blog
    3git remote -v
  5. Remove GitHub related files in your Hugo directory like these:

    • cname
    • .github/workflows/pages.yml
    • remove .github/ from .gitignore list
  6. Clear the Git cache with git rm -r --cached .github

  7. Push the local repo to the newly set remote origin with git push -u origin master.

  8. Your static site source code has now migrated from GitHub over to the new Git forge at git.sr.ht.

Step 2: Replace GitHub Actions (pages.yml) with SourceHut (build.yml)

The manifest file, build.yml, is the SourceHut equivalent of pages.yml from GitHub Actions. Instead of pushing the/public Hugo folder to the GitHub Pages repo (username.github.io) for free static site hosting, I have set the workflow to push the static files to Bunny.net for hosting using Bunny Storage.2

DISCLAIMER:

I am not an expert in automated workflows or cybersecurity by any means. I only achieved this workflow through trial and error. I’ve tried my best to research and ensure everything remains secure, but the code I share below may not be the best or safest method.

If you spot any red flags or places for improvement, I’d appreciate it a lot if you can let me know by leaving me a message, thanks!

To do this, we need to store some Bunny.net secrets in SourceHut Builds’ secrets manager so we could use them in the deployment process/manifest file.

Warning

Never hardcode any passwords, API keys, credentials, access tokens, etc into CI/CD workflows! Always use the platform’s secret manager to store the sensitive info. Read this article for more information about secrets in SourceHut manifests.

Storing Secrets in SourceHut Builds

I will assume you already have a Bunny.net account (they do offer a generous 14 day trial without credit card)2 and have a Storage zone3 and CDN pull zone set up (with the CDN origin set to the Storage Zone in question). Add these four Bunny secrets to the secrets dashboard:

  • BUNNYNET_API_KEY

    • Bunny main account API key, accessed by clicking on your Account icon (top of left sidebar) > API key
  • BUNNYNET_PULL_ZONE_ID

    • Numeric Pull Zone ID, accessed from the menu button on the top right corner of the CDN > Pull zone dashboard
  • BUNNYNET_SFTP_PASS

    • Bunny Storage zone password (not the read-only password), accessed from the Storage > “FTP & API access” dashboard (read extra step below)
  • BUNNYNET_SFTP_USER

    • Bunny Storage zone username, accessed from the Storage > “FTP & API access” dashboard

Note

There’s one extra compulsory step required for the Builds manifest to work later when using BUNNYNET_SFTP_PASS with rclone.

Instead of pasting the Bunny Storage read/write password directly into the SourceHut Secrets manager page, paste the output of:

1rclone obscure <paste-bunny-storage-password-without-brackets>

into the Secret field for BUNNYNET_SFTP_PASS when creating the SFTP password secret.

Tip: Run this locally in Terminal. Install rclone if it’s not installed yet with brew install rclone (for those who are using Homebrew).

After noting down the four credentials at Bunny.net, go back to SourceHut to add the secrets. This was a bit confusing for me at first, but these settings worked for me:

Screenshot of secrets in builds Sourcehut.com.

  • Name: I used capitals and descriptive names, but you can name the secrets anything you want. The generated UUID can be used to recall the secrets, but for the sake of clarity, I am using a meaningful name like BUNNY_SFTP_USER.

  • Secret: Paste the secure credential here, ensuring there are no extra spaces, quotes, line breaks, etc.

  • Secret Type: Choose File.

  • Path: I used a generic path like /tmp/SECRET_ONE. This is where the file will be stored in the VM, during the building process.

  • Mode: This is the permissions mode, AKA chmod. 600 ensures only the build user can read/write the secret file.

  • File: Leave blank, no need to upload an actual file.

Step 3: SourceHut Builds Manifest Template

Here is my builds.yml for your reference. I added some comments directly in the .yml for clarity, but here are some important notes I suggest reading before using the template:

  • Tailor the packages list for your own needs, e.g., no need for npm if Node.js is not in your workflow.

  • If the Git repo is set to private mode, do the following:

Build

  • The build section is straightforward; it generates the blog by running the Hugo build command.

    • The next command may not be required in your case, but because I didn’t integrate Pagefind’s indexing process into the theme itself, I have to run npx -y pagefind --site public every time after Hugo finishes building the site.

Deploy

  • The deploy section involves more steps:

    1. Set the path to the secrets

    2. Set a new variable called CONFIG_FILE. This will be our temporary (with limited access) rclone.conf file, used to connect to Bunny Storage. The .XXXXXX is a random suffix generator added to create a unique filename, preventing filename collisions.

    3. Add a trap to delete the CONFIG_FILE and remove the secret variables on exit (or earlier, even, if there is a failure).

    4. Add a loop to check every secret’s existence; if any are missing, it prints which one and exits with failure so the build stops early.

    5. Read secrets and make them accessible for creating rclone.conf.

      Tip

      It is good practice to rotate (regenerate) secrets frequently, use the lowest possible permission level, and reduce their exposure to different steps. I haven’t been able to get SourceHut (file type) secrets to auto-inject into the build environment (which is probably the safest way). If you know of a better/safer method, please let me know!4

    6. Notes about rclone.conf:5

      • Use port 22 for SFTP (not port 21 for FTP)

      • Find the host in the Bunny Storage dashboard or check the list on Bunny Storage docs.

      • cat > "$CONFIG_FILE" <<-EOF is the command to create the rclone.conf file with multi-line, indented text within our script. Ensure there’s a dash before EOF, as it tells the shell to remove the leading tab indents from the input. The .conf file will not work if this dash is not present!

      • use-agent and key_use_agent are both set to false to prevent rclone trying to connect using SSH key authentication.

    7. Now for the actual rclone sync command—this probably took the longest for me to debug, but the solution is simple!

      • We need to work around Bunny’s limitation of inability to rename files through FTP. Rclone, by default, uploads files by renaming them temporarily with a .partial suffix during the transfer. This would cause the sync to Bunny fail. By using the --inplace flag, rclone will upload directly without renaming files.

      • rclone sync blog-name/public/ bunny: means sync from source to destination, source being blog-name/public/ and destination being bunny: (Bunny Storage’s root directory).

      • The -sftp-set-modtime=false flag is recommended by Bunny.net customer support. --progress is useful for monitoring or use --stats-one-line for a more minimal log output. Read about other rclone flags like –checkers or –transfers.

    8. Last step is to clear the cache at Bunny CDN so the newly synced static site files are reflected online using curl.

Important

Make sure you are not revealing your secrets in logs or output, as this means your credentials are compromised/leaked to the public. Read this section in the builds.sr.ht documentation carefully and disable logging (xtrace) by using set +x when reading or using secrets!

Template

Remember to update blog-name (twice) to your own project’s directory, use your git source URL, secret UUIDs, and correct hostname in the rclone.conf when using the template!

  1image: alpine/latest
  2packages:
  3  - hugo
  4  - git # Hugo submodule updates
  5  - npm # Pagefind
  6  - rclone # Deployment to Bunny Storage
  7  - openssh-client # SFTP connection to Bunny Storage
  8  - curl # Bunny CDN cache purge
  9sources:
 10  - git@git.sr.ht:~username/repo_name
 11secrets:
 12  - 10f134e5-7889-48hd-a34k-bf179h5d2eq0 # Paste your SSH Key UUID for private repo access by Builds
 13  - BUNNYNET_API_KEY
 14  - BUNNYNET_PULL_ZONE_ID 
 15  - BUNNYNET_SFTP_PASS 
 16  - BUNNYNET_SFTP_USER
 17tasks:
 18  - build: |
 19      #!/bin/sh
 20      set -e
 21      echo "🚀 Starting build..."
 22
 23      # Change directory to Hugo site's root
 24      cd blog-name
 25
 26      echo "🚀 Building Hugo site..."
 27      hugo --gc --cleanDestinationDir --minify
 28      echo "✅  Hugo build complete."
 29
 30      echo "🚀 Building Pagefind database..."
 31      npx -y pagefind --site public
 32      echo "✅  Pagefind build complete."
 33
 34  - deploy: |
 35      #!/bin/sh
 36      set -e
 37      echo "🚀 Starting deployment..."
 38
 39      # Load secret file locations
 40      BUNNYNET_SFTP_PASS_PATH=/tmp/bunnynet_sftp_pass
 41      BUNNYNET_SFTP_USER_PATH=/tmp/bunnynet_sftp_user
 42      BUNNYNET_API_KEY_PATH=/tmp/bunnynet_api_key
 43      BUNNYNET_PULL_ZONE_ID_PATH=/tmp/bunnynet_pull_zone_id
 44
 45      echo "🚀 Creating temporary rclone.conf..."
 46      CONFIG_FILE="$(mktemp /tmp/rclone.conf.XXXXXX)"
 47      # Make CONFIG_FILE only readable by build user
 48      chmod 600 "$CONFIG_FILE"
 49
 50      # Ensure cleanup on exit even if the script errors out (remove temp config, unset secret variables)
 51      cleanup() {
 52        set +x
 53        rm -f "$CONFIG_FILE"
 54        unset SFTP_PASS SFTP_USER API_KEY PULL_ZONE_ID
 55      }
 56      trap cleanup EXIT
 57
 58      # Fail early if any secret file is missing to avoid writing incomplete config
 59      for p in "$BUNNYNET_SFTP_PASS_PATH" "$BUNNYNET_SFTP_USER_PATH" "$BUNNYNET_API_KEY_PATH" "$BUNNYNET_PULL_ZONE_ID_PATH"; do
 60        [ -f "$p" ] || { echo "Missing expected secret file: $p" >&2; exit 1; }
 61      done
 62      echo "✅ Secrets loaded successfully."
 63
 64      # Read secrets with xtrace logging disabled and create rclone.conf
 65      set +x
 66      SFTP_USER="$(< "$BUNNYNET_SFTP_USER_PATH")"
 67      SFTP_PASS="$(< "$BUNNYNET_SFTP_PASS_PATH")"
 68      API_KEY="$(< "$BUNNYNET_API_KEY_PATH")"
 69      PULL_ZONE_ID="$(< "$BUNNYNET_PULL_ZONE_ID_PATH")"
 70
 71      cat > "$CONFIG_FILE" <<-EOF
 72      [bunny]
 73      type = sftp
 74      host = storage.bunnycdn.com
 75      port = 22
 76      user = ${SFTP_USER}
 77      pass = ${SFTP_PASS}
 78      use_agent = false
 79      key_use_agent = false
 80      EOF
 81
 82      # Remove secret variables from shell after creating rclone.conf
 83      unset SFTP_USER SFTP_PASS
 84      echo "✅ Temporary rclone.conf created."
 85
 86      # Deploy to Bunny using rclone
 87      echo "🚀 Deploying to Bunny Storage..."
 88      {
 89        # Enable xtrace for rclone command only
 90        set -x
 91        rclone --config "$CONFIG_FILE" sync blog-name/public/ bunny: --progress --inplace --sftp-set-modtime=false
 92        set +x
 93      }
 94      echo "✅ Deployment complete."
 95
 96      # Purge BunnyCDN Cache
 97      echo "🚀 Purging BunnyCDN cache..."
 98      set +x
 99      curl -fsS -X POST "https://api.bunny.net/pullzone/${PULL_ZONE_ID}/purgeCache" \
100          -H "AccessKey: ${API_KEY}"
101      echo "✅ BunnyCDN cache purged."
102
103      # Cleanup is handled by trap above
104      echo "🧹 Cleanup scheduled on exit complete."

Step 4: Bunny.net Configuration: Bunny Storage and Bunny CDN

The static site files inside the /public folder should now all be uploaded to the root of the Bunny Storage zone.

  • Go to Delivery > CDN > Add Pull Zone.
  • Select Origin type to be the Storage zone with the files pushed from SourceHut.
  • Tick “Force SSL” on the General > Hostnames page and also on General > Origin > “Verify origin SSL certificate”
  • Add a custom hostname if you have a custom domain. You’ll need to go to your registrar and add a CNAME6 pointing to your storage-zone-name.b-cdn.net. Tick “Force SSL” when it’s been added.

Screenshot of Bunny.net usage statistic dashboard view.

Site has been up and running smoothly ever since I migrated

Conclusion

ALTTEXT

Slimmer tech stack and faster loading times after leaving GitHub Pages

Your static site should now be:

  • Served to readers faster (based on my own speed test comparisons) than when hosting on GitHub Pages
  • You’ve gained the ability to control security headers, cache, and other CDN-related settings (without relying on a giant like Cloudflare CDN).
  • It relies on EU-based services that respect privacy.

Thanks for reading my guide on how to switch from hosting a static site from GitHub Pages to SourceHut and Bunny Storage! I hope you found this migration guide useful. I think it’s a good time to leave or rely less on Microsoft’s GitHub, especially after reading the news that Thomas Dohmke will official step down as GitHub CEO.

This is the most thorough guide I’ve written. If you benefited from it, I’d appreciate it a lot if you would consider supporting me by using my Bunny.net affiliate link (which will directly support the hosting costs of this blog), or buying me a coffee with the links at the bottom of the page. Thanks!

And let me know how your migration went by leaving a comment, I’d love to hear from you.


  1. My first time using Mermaid diagrams. It’s so cool; plus my Hugo theme, Anubis2 supports it! ↩︎

  2. If you’re interested in trying Bunny.net’s services, consider using my affiliate link to show your support. I also found a promo code that extends the trial to over a month; but I’m not sure how long it will last: JSMASTERY↩︎ ↩︎

  3. Note that when creating a Storage zone on Bunny.net, “GEO replication” can only be added, not removed. So start off with less! ↩︎

  4. Good post about security of secrets in CI/CD workflows. ↩︎

  5. Originally, I wanted Rclone to connect directly to Bunny Storage with an API, without SFTP, using a built-in configuration. However, Rclone (version 1.70.3) does not currently support Bunny.net in its backend, which is why I’m using SFTP instead. ↩︎

  6. Read about Bunny’s take on ANAME records↩︎

Read the original on burgeonlab.com

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.