Why I Ditched My Self-Hosted Runner for GitLab Shared Runners — and Made It Work on ARM Chromebooks
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:
image: debian:stable-slim— works on x86_64 and ARM64.- Pinned versions (
BB_VERSION, renderer version) for reproducibility. - A
casestatement mapsuname -mto the correct release archive name. - Smoke tests after each install so the failure is obvious.
- The same cache key as before, so warm builds still skip redundant work.
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:
- Install the ARM64 GitLab runner on the Chromebook if I want a local build.
- 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.