Published: 23 September 2026
CI minutes are the one line on an engineering bill that grows without anyone deciding it should. You add a matrix dimension, someone enables a nightly job, a flaky test gets a retry, and three months later the hosted-runner invoice is larger than your entire production infrastructure. Self-hosted runners fix the price problem. Putting them on an hourly VPS fixes the second problem — paying for a build machine at three in the morning when nothing is building.
The shape of the idea
A self-hosted CI runner is a small daemon that polls your forge for queued jobs, runs them, and reports back. GitHub Actions calls it the runner, GitLab calls it the GitLab Runner, Woodpecker and Drone have their own agents. All of them are the same thing architecturally: a long-lived process on a machine you control, reaching out over HTTPS. No inbound firewall rules, no public IP requirement, no exposure.
That property is what makes hourly billing work. The runner does not need a stable address, so the machine underneath it is disposable. Create it when the working day starts, destroy it when the queue empties, and pay for the hours that existed.
Sizing the machine
Runner sizing is dominated by two things: how much your build wants to hold in memory, and how many jobs you want to run at once. A rough guide against our hourly VPS plans:
| Plan | RAM / vCPU / SSD | Sensible for | Per hour (Bucharest) |
|---|---|---|---|
| VPS II KVM | 4 GB / 2 / 40 GB | One runner: linting, unit tests, small Go or Rust crates, container builds of modest images | €0.0083 |
| VPS III KVM | 8 GB / 2 / 60 GB | One busy runner: Node or Python monorepos, Docker builds with a warm layer cache | €0.0139 |
| VPS IV KVM | 16 GB / 4 / 80 GB | Two to four concurrent runners, or one JVM/Android build that wants heap | €0.0222 |
| VPS V KVM | 24 GB / 6 / 100 GB | A shared team runner host, four to six executors | €0.0417 |
| VPS VI KVM | 32 GB / 8 / 120 GB | Heavy parallel matrices, large container builds, link-heavy C++ or Rust | €0.0806 |
Disk is the constraint people underestimate. A Docker layer cache, a node_modules cache and a few checkouts of a real repository will eat 40 GB without trying. If your builds are container-heavy, start at the 60 GB plan and prune aggressively.
The arithmetic that makes this worth doing
Take a team running builds roughly from 08:00 to 20:00 on weekdays. That is 12 hours × 5 days × about 4.3 weeks, or roughly 258 hours a month, against 720 hours for a machine that never turns off.
| Setup | Hours billed | Cost per month |
|---|---|---|
| One 16 GB runner, always on (monthly cycle, Bucharest) | — | €16.00 |
| One 16 GB runner, weekday working hours only, hourly | 258 | €5.73 |
| Three 16 GB runners, weekday working hours only, hourly | 774 | €17.19 |
| Three 32 GB runners, weekday working hours only, hourly | 774 | €62.38 |
The headline is the third row: three sixteen-gigabyte build machines, available every working hour of the month, for about what one of them costs left running. That is the trade hourly billing actually buys you — not a smaller bill for the same capacity, but more capacity for the same bill.
Wiring it up
1. Build one machine by hand, then stop doing that
Create a VPS, install the runner, register it, run a build, and confirm it works end to end. Then capture what you did as a script — cloud-init, an Ansible play, a shell script in the repository, whatever you already use. The manual pass exists only to write the script.
2. Register with a token, not a snapshot
Do not bake a runner registration token into an image. Both GitHub and GitLab issue short-lived registration tokens through their APIs; fetch one at boot, register, and let the runner deregister itself on shutdown. A stale runner entry in your forge that no longer exists is how jobs end up queued forever.
3. Give each runner a label that means something
Label by capability and by location —
linux-x64, docker, eu-ro. When you later add a second machine in another country, the labels are how you steer jobs at it without editing every workflow.4. Decide what happens to the cache
This is the design decision that determines whether ephemeral runners are a win. A runner that starts from nothing every morning re-downloads every dependency. Options, roughly in order of effort:
- Keep the machine for the working day, not per job. The cache survives the day, and you still skip nights and weekends. Simplest, and usually enough.
- Run a caching proxy on a small always-on VPS — a registry pull-through cache, an npm or PyPI mirror — and point the ephemeral runners at it. A €4.00 2 GB machine in Bucharest does this comfortably.
- Use your forge's own cache backend if it has one and the egress is free.
5. Put the runners near what they talk to
If your builds pull from a registry or artifact store in a particular region, put the runners in the location with the shortest path to it. Our five VPS locations are Bucharest, Warsaw, Amsterdam, Frankfurt and Vienna; Frankfurt and Amsterdam have the deepest peering and are usually the safe default for talking to the wider internet, while Bucharest is the cheapest and perfectly good when the bottleneck is CPU rather than the network. The per-location prices are in the location pricing post.
Things that will bite you
- A stopped VPS still bills. Our meter charges for a virtual machine that exists, because a powered-off VM still holds its RAM allocation, its disk and its IP on a node. If your automation "stops" runners overnight instead of deleting them, you will pay the full monthly figure and gain nothing. Destroy them.
- Credit, not an invoice. Hourly services draw down prepaid account credit once per clock hour, at the top of the hour in UTC. If credit runs out there is one hour of grace before suspension and 48 hours before termination — enough to notice, not enough to ignore. Turn on automatic top-up if your CI pattern is spiky.
- Secrets on disposable machines. An ephemeral runner is a fine place to leak a credential if you copy long-lived secrets onto it. Prefer short-lived tokens issued at job time.
- Runner isolation. Self-hosted runners and untrusted pull requests are a bad combination. Keep fork-triggered builds on hosted runners, or gate them behind approval.
Where this sits next to a test grid
CI runners and a browser-test grid are different machines with different appetites — runners are CPU- and disk-hungry, a Playwright or Selenium grid is memory-hungry and bursty. Running them on one box means the browsers evict your build cache. We covered the grid side separately in self-hosting a Playwright or Selenium grid; the two posts compose if you want the whole pipeline in-house.
Start with one
Pick the 8 GB or 16 GB size on the hourly VPS page, choose a location, and register a single runner against one noisy repository. Measure a week. If the queue times improve and the bill is a rounding error, script it and add the other two. Full root, full KVM, and an in-browser console when you inevitably break the network configuration at 23:00.