DEV Community

Cover image for Owning Your GitHub Actions CI: Moving to Self-Hosted Runners
Iuri Covaliov
Iuri Covaliov

Posted on

Owning Your GitHub Actions CI: Moving to Self-Hosted Runners

Moving to self-hosted runners isn’t just about saving CI minutes.
It’s about shifting from using infrastructure to operating it.

For a long time, GitHub-hosted runners were simply the default. You push code. A workflow runs somewhere. Artifacts appear. Clean. Abstracted. Convenient.

I had experimented with self-hosted runners before — mostly while learning GitHub Actions. It worked, but I didn’t see a compelling reason to keep it.

Then two things changed.

First, I once had to build a macOS executable for a small utility. The binary produced by the default runner didn’t work on two developers’ laptops. We ended up building it directly on one of their machines. That was the first time I clearly felt that CI environments are not neutral. They are specific systems with specific assumptions.

Second — more recently — GitHub runner minutes stopped being effectively free.

That’s when experimentation turned into architecture.



Not Just Installing a Runner

Installing a runner is easy.

Designing a small, intentional self-hosted CI layout is different.

I structured the lab around:

  • A Linux server (Ubuntu 24.04 in my case)
  • Separate runners for:
    • Personal repositories
    • Organization repositories
  • A clear directory structure:
/opt/gh-actions-runners/
  personal-runner-1/
  organization-runner-1/
Enter fullscreen mode Exit fullscreen mode
  • Explicit labels for routing jobs
  • Docker-based build workflows

The goal wasn’t to recreate GitHub’s infrastructure.

It was to remove invisible layers and understand the system I was relying on.

That difference matters.


Reality Check #1: Organizational Boundaries Are Part of Runtime

At one point everything looked correct:

  • Runner online
  • Labels matching
  • Workflow targeting self-hosted, Linux, X64, ci

And yet the job remained stuck:

Waiting for a runner to pick up this job...
Enter fullscreen mode Exit fullscreen mode

No obvious error. No crash. Just silence.

The issue wasn’t YAML.
It wasn’t the runner service.

It was a runner group setting: public repositories were not allowed to use that runner.

Once enabled, the job started immediately.

It was a small issue — but revealing.

Self-hosted CI expands the surface area of configuration. Policy, permissions, and organizational settings become part of runtime behavior.

When you host it yourself, abstraction doesn’t disappear. It just moves.


Reality Check #2: Docker Isn’t Just Docker

In another project outside the lab, I ran a simple workflow:

  • docker build
  • docker push

Nothing complex.

Everything in the runner workspace looked correct.
ls showed the Dockerfile.
Permissions were fine.
The runner service was healthy.

And still, builds failed with:

lstat /var/lib/snapd/void/... no such file or directory
Enter fullscreen mode Exit fullscreen mode

The problem wasn’t the workflow.

Docker had been installed via Snap.
The runner workspace lived under /opt.
Snap confinement prevented Docker from accessing that path.

From the shell, everything looked fine.
From Docker’s perspective, the directory simply did not exist.

The fix was straightforward:

  • Remove Snap Docker
  • Install Docker via apt
  • Restart the runner service

But the lesson wasn’t about Docker.

It was about environmental assumptions.

When you move to self-hosted CI, packaging decisions become architectural decisions.


Reality Check #3: Ephemeral Is a Luxury

GitHub-hosted runners are ephemeral.
They clean themselves after every job.

Self-hosted runners do not.

After a handful of Docker builds, disk usage starts to grow:

  • Images
  • Layers
  • Build cache
  • Stopped containers

Nothing breaks immediately.
But entropy accumulates quietly.

So the lab gained maintenance scripts:

  • A recommended cleanup routine
  • An aggressive recovery option
  • Optional cron automation

Owning CI means owning lifecycle — not just pipelines.


What Actually Changes

Moving to self-hosted runners isn’t primarily about saving money.

It’s about shifting responsibility.

You gain:

  • Predictable environments
  • Full control over toolchain versions
  • No external minute limits
  • Transparency in how jobs are executed

You also gain:

  • Infrastructure maintenance
  • Disk management
  • Docker lifecycle awareness
  • Organizational configuration complexity
  • Security considerations (your runner executes arbitrary code)

It’s not better or worse.

It’s simply more explicit.


Lab Repository

The full lab setup — including:

  • Runner installation runbook
  • Architecture notes
  • Example workflows
  • Docker cleanup scripts

is available in the accompanying repository:

👉 https://github.com/ic-devops-lab/devops-labs/tree/main/GitHubSelfHostedRunners


There’s a difference between using CI and operating CI.

Even at small scale, that difference is meaningful.

Top comments (0)