NoiXdev
← All projects
in progress

Portainer GitHub Runner

A self-hosted GitHub Actions runner as one Portainer stack: two services, one environment block, no build step. Moves the single long CI job onto your own hardware — with the two safeguards such setups usually lack.

What it is

GitHub does not bill self-hosted runner minutes, so moving a long CI job onto your own machine takes it off the bill. This project packages that as a Portainer stack: point it at a repository, fill five variables, deploy.

The arithmetic only works for long jobs, though. Moving a two-minute typecheck saves nothing and costs you a machine to look after.

Why

A worked example: a repository whose push cost 60 billed minutes — 52 of them a single browser-test job. Moving that one job removed 87 % of the bill. The four fast jobs stayed where they were, so a push still gets its verdict when the server is off.

The two rules it encodes

Both exist because their absence produces failures that do not name their cause.

Identical paths. The runner uses the host’s Docker daemon, so every path it hands over is resolved host-side. If the work directory is not mounted at its own host path, Docker silently creates empty directories and mounts those. Nothing errors — you just get a container that starts and serves nothing, or a build that cannot find files that are plainly there.

A filtered prune. A persistent runner accumulates one image per branch build plus every container a job forgets to tear down, and nothing on a Docker host removes them. The filter matters as much as the prune: an unfiltered docker image prune -af deletes exactly the images the next job needs and adds their download to every run.

Highlights

  • One stack, two services — the runner and the prune, one environment block
  • A work directory that bind mounts actually resolve through
  • Hourly prune of images, build cache and stopped containers, filtered to older than 24 hours
  • Volumes left alone unless explicitly allowed — on a shared host a volume prune destroys other stacks’ data
  • cleanup.sh to reset a host: reports first, changes nothing until you name a scope
  • Opts out of auto-updaters — a Watchtower that restarts the runner mid-job loses the job and reports it as cancelled
  • Documents what it cannot do: state in a reused volume, such as a seeded test database, is invisible to any prune — that belongs in the workflow, before and after the job

The risk, stated plainly

The runner mounts the host’s Docker socket, and that is root on the host. Any workflow it executes can start a privileged container and read or write anything on the machine. There is no sandbox, and none is intended: the design assumes you trust every workflow in the target repository.

For a private repository with trusted authors, on a host that carries nothing precious, that is a reasonable trade. For a public repository, or one that accepts pull requests from forks, it is not — a stranger’s pull request would run as root. Anyone who needs isolation rather than trust is pointed at actions-runner-controller in the README.

Status

Active development — not proven in production yet. It is in use and being shaped by that use, but no job has run end to end on it: the first real deploy crash-looped and turned up three configuration bugs in the repository, since fixed.

Specifically unproven, worst first: actions that install tooling (written for GitHub’s images, and inside a container they usually fail without a reason), a complete job, and whether a 24-hour prune window is the right trade over weeks.

Pin a commit rather than tracking main if a surprise would cost you something. Issues and pull requests welcome — github.com/NoiXdev/portainer-github-runner.