I was convinced self-hosting my GitLab runner was the perfect hack. My laptop became the build box, the quota stayed at zero, and every deploy felt like a personal flex. Then my laptop went to sleep. A push sat in the queue for fifteen minutes before I noticed the fan had stopped. The runner died because the shell died, and I realized I had traded one problem for another.


The Tradeoff I Was Avoiding

Two weeks ago I wrote about how my self-hosted runner saved me from GitLab’s 400-minute quota. The logic was sound: GitLab only counts minutes on its own shared runners, so a runner on my own hardware burns nothing against the cap.

Dimension Shared runner Self-hosted runner
Quota cost Burns 400 min cap $0 against quota
Reliability Always on Dies when my shell dies
Speed DinD + image pull Native tools, warm cache
Best for Teams, always-on builds Hobby projects I babysit

The hidden cost was babysitting. Every reboot, every shell crash, every laptop close — I had to remember to restart the runner. That is fine until you want to push from a coffee shop, a phone, or a Chromebook that does not even have the runner binary installed.


Option B: Install Everything in CI

I picked the option I originally called slower: install Babashka, pandoc, and the diagram renderer inside the CI job. That way the runner does not matter. Any GitLab runner with a Docker executor can pick up the job.

The key insight: the tools are small. The whole install step adds roughly one minute, but it removes the requirement that a specific machine be online.


The ARM / Chromebook Angle

This is the part I care about most. My daily driver is a Chromebook with an ARM chip. I want the same pipeline to work there without special-case setup. The good news is that every tool I need publishes aarch64 binaries.

Babashka’s install script auto-detects uname -m, so the same command works on both architectures. The diagram renderer’s release names follow the same linux-amd64 / linux-arm64 pattern, so I map x86_64 to amd64 and aarch64 to arm64 in a small shell case statement. Pandoc and the JRE come straight from Debian’s architecture-aware repositories. The result is a single .gitlab-ci.yml that runs on GitLab’s shared x86_64 runners and on a local ARM Chromebook running a GitLab runner.

Tool Source x86_64 ARM64
Babashka GitHub release linux-amd64-static linux-aarch64-static
Renderer GitHub release linux-amd64 linux-arm64
pandoc Debian apt amd64 package arm64 package
JRE Debian apt amd64 package arm64 package

The Hidden Dependency: Java

The first time I tried this, the build crashed inside Babashka with a NullPointerException. The smoke tests passed — bb --version worked — but bb build failed immediately.

The reason: my bb.edn declares a Maven dependency (hiccup). When Babashka resolves dependencies, it needs a Java runtime, even though Babashka itself is a native binary. The static bb binary does not bundle the JRE. Once I added default-jre-headless to the before_script install list, the build succeeded.

Installed package Why it is needed
curl Download install scripts and release archives
ca-certificates HTTPS trust for GitHub downloads
tar Extract renderer and Babashka archives
pandoc Convert Markdown posts to HTML
default-jre-headless Resolve :deps in bb.edn

What the New Pipeline Looks Like

The .gitlab-ci.yml is now short and explicit. No tags, no assumption about the host, no custom Docker image. The job starts from debian:stable-slim, installs the five packages above, downloads the two pinned binaries, and runs bb build followed by bb validate-links.

You can read the full file in the repository. The highlights are:


Cost and Speed Revisited

Approach Quota Cold build Warm build Reliability
Self-hosted shell $0 ~80s ~50s Tied to my laptop
Shared runner, install in CI ~1 min/push ~2–3 min ~1.5–2 min Always on
Custom CI image ~1 min/push ~45s ~45s Always on, more maintenance

The shared-runner approach costs minutes, but it removes the single point of failure. For a blog with irregular commits, the 400-minute free tier is still plenty. If I ever publish multiple times per day, I can still fall back to the custom CI image later.


Why This Matters on a Chromebook

A Chromebook running Linux on ARM cannot easily run an amd64 self-hosted runner binary. With the new pipeline, it does not need to. I can:

  1. Install the ARM64 GitLab runner on the Chromebook if I want a local build.
  2. Or push from anywhere and let GitLab’s shared runners handle it.

Both paths run the exact same job because the job itself installs the right architecture binaries. That is the real win: the build is no longer coupled to whichever machine happens to be awake.


The Lesson

Self-hosting is a great hack until the host goes to sleep. Installing tools in CI is slower per run, but it makes the pipeline a first-class citizen of the repository, not a pet process on my laptop. And because every tool ships aarch64 binaries, the same pipeline runs on x86_64 servers, ARM servers, and my ARM Chromebook without a single architecture check outside the install step.


Your CI should depend on your repository, not on which laptop remembered to stay awake.