Building this Blog on Push with Forgejo and Woodpecker

You might or might not know that this blog is hosted on a small single board computer in my basement and until recently I’d write or edit my posts, rebuild it locally and then push the built files up to the server hosting the static files. Works, but cumbersome and I’d frequently push drafts by accident. Not a big deal since drafts are also in Git, but still, unprofessional. But the beauty of a homelab is that you can automate things with as much or as little effort as you want. And automate I did.

I host an instance of the fantastic forgejo in my lab and the blog was one of the first projects I’ve moved there. However, the rebuild of the website kind of sucked, specifically if you’ve used Github’s pages features before. Push some raw files, and a worker automatically builds the website for you. Forgejo has the features needed for this, webhooks, but I didn’t have a runner to actually execute my tasks. For reasons that are not important and mostly come down to me “oh, that’s cool” I wanted my runners to be running on my kubernetes cluster to make use of available compute where possible. That narrowed down the list of options to Woodpecker, yet another fantastic open source project. Woodpecker’s agent has the ability to schedule workloads on Kubernetes, making it exactly what I was looking for.

Now the way it works is like this:

  1. A push to a project enrolled arrives at forgejo.
  2. Forgejo posts a webhook to woodpecker.
  3. Woodpecker reads the workflow files and picks up the workflows matching the current event (push).
  4. It submits a job to the agent which in turn provisions a volume via longhorn. Then it starts a pod, mounts the volume, and runs a git checkout of the project.
  5. The remaining steps are then executed one by one in a separate pod, each with the volume attached.
  6. In case of this blog, the last step invokes rclone to synchronize the built files up to the garage server.

And the end result… well, you’re looking at it.

Now, could this have been done in a simpler way? Absolutely. But it was an interesting exercise wiring up these components and overall they’re pretty lightweight; woodpecker’s pods amount for less than 100 MB of used memory and almost no CPU. The heaviest component in all of this is Longhorn and initially creating volumes took quite long until I realized it was provisioning volumes with 3 replicas which is unnecessary for this use-case. After I created a separate storage class, volumes are provisioned in 10 seconds or less, making builds reasonably fast:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: longhorn-ci
provisioner: driver.longhorn.io
parameters:
  numberOfReplicas: "1"
  dataLocality: "strict-local"
  staleReplicaTimeout: "1"

Woodpecker agent then needs to be configured to use that storage class: BACKEND_K8S_STORAGE_CLASS: "longhorn-ci".

Overall I’m very happy with this. Blogging became a little easier and I’m already exploring what else I can do with my own CI runners.

Edit: Case in point, I pushed this post up as a draft and it didn’t publish it. Yay.

ilikeorangutans

Jakob Külzer’s personal blog


2026-09-29