Self-Hosted CI Runners on an Hourly VPS in 2026

September 23, 2026Mario Marin
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:
PlanRAM / vCPU / SSDSensible forPer hour (Bucharest)
VPS II KVM4 GB / 2 / 40 GBOne runner: linting, unit tests, small Go or Rust crates, container builds of modest images€0.0083
VPS III KVM8 GB / 2 / 60 GBOne busy runner: Node or Python monorepos, Docker builds with a warm layer cache€0.0139
VPS IV KVM16 GB / 4 / 80 GBTwo to four concurrent runners, or one JVM/Android build that wants heap€0.0222
VPS V KVM24 GB / 6 / 100 GBA shared team runner host, four to six executors€0.0417
VPS VI KVM32 GB / 8 / 120 GBHeavy 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.
SetupHours billedCost per month
One 16 GB runner, always on (monthly cycle, Bucharest)€16.00
One 16 GB runner, weekday working hours only, hourly258€5.73
Three 16 GB runners, weekday working hours only, hourly774€17.19
Three 32 GB runners, weekday working hours only, hourly774€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.