Moving My Blog Deploys Over to Forgejo Actions

Introduction

This blog has had an automated deploy path for a couple of years now, so the basic idea isn’t new. Push content, build the static site, ship the generated output to the VPS, and let nginx serve the result. Nice and simple.

What is new is the plumbing behind it.

I’ve only recently moved over to Forgejo, and figured it was worth giving a quick overview of how the blog deploy works now that it’s running on Forgejo Actions instead of the older setup.

This post is also a handy proof that the migrated workflow is doing what it should. If you’re reading it on thomas.tremlett.dev, then Forgejo built the site, copied the generated output to the VPS, and the serving layer did its part.

The Goal

The goal wasn’t to reinvent the whole publish path. It was to preserve the parts that were already working while swapping in Forgejo as the thing coordinating the build and deploy.

In practice that meant:

  • keep Hugo as the thing that builds the site
  • keep Forgejo as the source of truth for the content
  • push the generated public/ output directly to the VPS
  • avoid turning a straightforward deploy into a science project just because I changed forge platforms

That’s not exactly a groundbreaking CI/CD architecture, but it doesn’t need to be. The whole point is reducing friction. The easier it is to push a post and trust the pipeline, the more likely I am to actually keep the blog current instead of letting drafts rot in the repo.

The Rough Shape of the Workflow

The current pipeline is intentionally boring, which is exactly what I want from deployment plumbing.

When I push to main, Forgejo Actions:

  1. checks out the repo
  2. installs Hugo and a few deploy tools
  3. builds the site
  4. connects to the VPS over SSH
  5. syncs the generated output into /srv/hugo-blog/public/
  6. confirms the local blog container is still serving content

That leaves the VPS doing what it should be doing, namely serving files, while Forgejo handles the build and orchestration side. Clean enough, easy to reason about, and not especially fragile.

What Changed With The Move

Most of the logic stayed the same. The main differences were around the workflow definition and runner behaviour.

The workflow now lives in .forgejo/workflows/, uses Forgejo’s checkout action, and authenticates to the VPS with repo-scoped secrets for the deploy key and pinned host keys. On paper that’s all pretty tame. In practice there were still a few mildly annoying migration details to work through.

The first run failed because the runner environment didn’t have sudo, which is one of those details that’s invisible right up until it burns a few minutes of your life. After that came the usual SSH key dance: making sure the right deploy key was installed, making sure the secret in Forgejo matched the actual private key I intended to use, and making sure the runner could authenticate the same way I could from my laptop.

So, in other words, a fairly standard infrastructure migration. Nothing dramatic, just the normal amount of fiddling required to move something from “works in principle” to “works reliably enough that I stop thinking about it.”

Why This Matters

I like static sites because they’re small, understandable, and hard to accidentally turn into a sprawling maintenance burden. Pairing that with Forgejo Actions makes the blog feel like a proper part of the broader lab rather than a weird side project running on vibes and manual file copies.

It also sets a cleaner precedent for the rest of the lab. Once one deploy lane has been migrated cleanly, it becomes much easier to apply the same pattern elsewhere: small workflows, explicit secrets, predictable runners, and a clean split between build, deploy, and restore responsibilities.

Closing Thoughts

This wasn’t a brand new automation journey so much as a platform transition with a bit of cleanup along the way.

The old idea still holds: write post, push change, let the system do the boring part. Forgejo is just the latest piece of infrastructure now responsible for making that feel seamless.

And if you’re reading this live on the site, then that handoff appears to be working just fine.