CI runner and staging VPS that you control end to end.
First month $1, then the normal monthly price. One per customer.
KVM, full root, NVMe RAID10, and a box meant for runners you host yourself. GitHub Actions, GitLab runners, Drone, Woodpecker, whatever the team already uses is fine. Staging and preview environments, the ones clients click through before signing off, can sit on the same server. The price is fixed per month, traffic is unlimited, and there's no per-minute bill growing quietly toward month end. Amsterdam or New York, pick whichever is closer to where the team actually works.
Transparent pricing
Plans and prices
| Plan | vCPU | Memory | NVMe | Traffic | Monthly | 1 year | |
|---|---|---|---|---|---|---|---|
| Standard | |||||||
| S-2Standard | 1vCPU | 2 GBMemory | 30 GBNVMe RAID10 | UnlimitedTraffic | $7.99/moFirst month $1 | $7.19/mo on 1 year | Start for $1 |
| S-4Standard | 2vCPU | 4 GBMemory | 60 GBNVMe RAID10 | UnlimitedTraffic | $13.99/moFirst month $1 | $12.59/mo on 1 year | Start for $1 |
| S-8Standard | 3vCPU | 8 GBMemory | 120 GBNVMe RAID10 | UnlimitedTraffic | $24.99/moFirst month $1 | $22.49/mo on 1 year | Start for $1 |
| S-16Standard | 4vCPU | 16 GBMemory | 240 GBNVMe RAID10 | UnlimitedTraffic | $44.99/moFirst month $1 | $40.49/mo on 1 year | Start for $1 |
| S-32Standard | 8vCPU | 32 GBMemory | 480 GBNVMe RAID10 | UnlimitedTraffic | $84.99/moFirst month $1 | $76.49/mo on 1 year | Start for $1 |
| S-64Standard | 16vCPU | 64 GBMemory | 720 GBNVMe RAID10 | UnlimitedTraffic | $159.99/moFirst month $1 | $143.99/mo on 1 year | Start for $1 |
| Dedicated CPU | |||||||
| D-4Dedicated | 1vCPU | 4 GBMemory | 45 GBNVMe RAID10 | UnlimitedTraffic | $37.00/moFirst month $1 | $31.45/mo on 1 year | Start for $1 |
| D-8Dedicated | 4vCPU | 8 GBMemory | 90 GBNVMe RAID10 | UnlimitedTraffic | $69.00/moFirst month $1 | $58.65/mo on 1 year | Start for $1 |
| D-16Dedicated | 4vCPU | 16 GBMemory | 160 GBNVMe RAID10 | UnlimitedTraffic | $118.00/moFirst month $1 | $100.30/mo on 1 year | Start for $1 |
| D-32Dedicated | 6vCPU | 32 GBMemory | 220 GBNVMe RAID10 | UnlimitedTraffic | $215.00/mo | $182.75/mo on 1 year | Deploy D-32 |
| D-64Dedicated | 10vCPU | 64 GBMemory | 400 GBNVMe RAID10 | UnlimitedTraffic | $377.00/mo | $320.45/mo on 1 year | Deploy D-64 |
| High Memory | |||||||
| H-64High Memory | 8vCPU | 64 GBMemory | 480 GBNVMe RAID10 | UnlimitedTraffic | $249.00/mo | $211.65/mo on 1 year | Deploy H-64 |
| H-128High Memory | 16vCPU | 128 GBMemory | 960 GBNVMe RAID10 | UnlimitedTraffic | $459.00/mo | $390.15/mo on 1 year | Deploy H-128 |
| H-192High Memory | 24vCPU | 192 GBMemory | 1.4 TBNVMe RAID10 | UnlimitedTraffic | $669.00/mo | $568.65/mo on 1 year | Deploy H-192 |
Why a runner on a fixed-price VPS pays off.
We ran hosted minutes ourselves for about a year. Month one looked fine. Then someone added a matrix build, then a second repo joined, and by spring the invoice had a line on it nobody could explain in under five minutes. That is usually when a team calls us. A runner you host costs the same on the first of every month, and its cache is still warm from yesterday.
A warm cache is the real speedup
Here is the thing people underestimate. A hosted runner starts every single job from a blank machine. No Docker layers, no npm cache, no cargo registry, nothing. So a job that should take ninety seconds spends four minutes downloading the same things it downloaded an hour ago. Put the runner on your own box and all of that stays on the NVMe disk between jobs. Only the first run is slow. After that the downloads simply do not happen.
Flat monthly price, no per-minute meter
What you pay is the plan price. There is no second number. Traffic is unlimited on every plan, so a pipeline that pulls two gigabyte images and pushes artifacts all afternoon changes nothing on the bill. And the renewal is the price you signed up at, we do not do the quiet increase after year one.
Staging that looks like production
Most agencies we host do something like this: one compose stack per client on the same server, each behind the reverse proxy on its own hostname. The last job in the pipeline deploys into the client's stack and posts the link. The client clicks it. That is the whole handover. No zip files, no screen recordings, no meeting to show the thing.
Preview environments from the API
Want a preview per branch instead of per client? Same idea, one step further. The pipeline calls our REST API, creates a small VPS with cloud-init, and drops the URL into the pull request. When the branch merges, the same pipeline deletes the server again. A new server is up in 30 to 60 seconds, which is faster than most people can open the PR.
Which plan for which pipeline.
CI load is strange. The server idles for hours, then for four minutes every core is pinned. You size for those four minutes, not for the average, and you size the disk with more care than the CPU, because caches grow until something stops them.
| Setup | Plan | Why |
|---|---|---|
| A single runner for tests and lint, no Docker builds | S-4 | 2 vCPU is the realistic floor, and 60 GB holds one project's caches |
| A single runner doing Docker builds with a layer cache | S-8 | 3 vCPU takes parallel jobs, 120 GB takes images and caches |
| A runner next to two or three client staging stacks | S-16 | With 16 GB the staging databases stay resident during a build |
| Compile-heavy work such as Rust, C++, Android and large TypeScript monorepos | D-8 | 4 pinned cores, so build times stop varying |
| Team with many concurrent jobs | D-16 or S-32 | Extra cores buy concurrency, extra RAM buys parallel containers |
The smallest plan we will put a runner on is the S-4: 2 vCPU, 4 GB, 60 GB NVMe, €11.99 a month ($13.99). One vCPU is fine for lint. It is not fine for a test suite that runs in parallel, or for Docker builds, those just queue up behind each other and the whole pipeline crawls. On the S-4 the thing that runs out first is not CPU but disk. A layer cache for a handful of images, a node_modules directory and a Rust target folder get to 60 GB in a few weeks. We have watched it happen.
So for most teams we say S-8. That is 3 vCPU, 8 GB and 120 GB for €21.99 a month ($24.99). With three cores the runner takes two jobs at once, and 120 GB is enough cache to notice the difference. If the staging stacks live on the same server, go one up to the S-16 (4 vCPU, 16 GB, 240 GB) at €39.99 a month ($44.99). Then the staging Postgres and Redis stay in memory while a build hammers the box next to them.
Compile-heavy work is where shared vCPU shows its face. Standard plans share cores 1:4, so a long Rust build takes eleven minutes on Monday and fourteen on Thursday, depending on the neighbours. That drives people mad when the build gates a merge. The D-8 (4 vCPU, 8 GB, 90 GB) at €64.00 a month ($69.00) pins four EPYC Genoa cores 1:1, and then the same build takes the same time every run. Note the disk though: 90 GB. On Dedicated plans another 100 GB costs €16.00 a month, and a compile cache usually wants it. Outgrowing a plan is not a migration, by the way. We resize RAM and vCPU in place, data and IP stay, usually with one short reboot.
Setting up a runner on BuyVPS.
Step 1: order, pick Ubuntu 24.04, install Docker
Order the S-8, or start on the $1 trial month if you want to see it run first. Pick Ubuntu 24.04 and a region. Log in as root, update the box, and install Docker with the official convenience script. Every runner below uses Docker, either as its executor or for the builds, so it goes on first.
Step 2: GitHub Actions runner
In GitHub go to the repository or the organisation, then Settings, Actions, Runners, New self-hosted runner. GitHub hands you the download URL, the version and a registration token, and that token expires quickly, so do not go for coffee in between. One thing catches almost everyone: the runner refuses to start as root. Create a runner user first, add it to the docker group, and do the download and the registration as that user.
Register it once with config.sh and then install it as a service with the svc.sh script that ships with the runner. systemd owns it from there: the runner comes back after a reboot and gets restarted if it dies halfway through a job.
Then set runs-on: [self-hosted, vps] in the workflow. Add --ephemeral to config.sh if the runner should take exactly one job and then deregister. The runner source and releases are at github.com/actions/runner.
Step 3: GitLab runner with the Docker executor
GitLab has its own apt repository for gitlab-runner, and that is the version to use, not whatever apt on Ubuntu offers. Register with the runner authentication token from the project's CI/CD settings and choose the Docker executor. Each job then starts in a fresh container, which is the only thing standing between job A and the files job B left behind.
In config.toml two settings matter more than the rest: concurrent, which sets how many jobs run at once, and the executor image the jobs start from. Keep concurrent one below the vCPU count, so the host still has a core to breathe with.
To build images inside jobs, either mount the host socket by adding --docker-volumes /var/run/docker.sock:/var/run/docker.sock to the register command, or use docker-in-docker with --docker-privileged. The socket mount shares the layer cache; docker-in-docker is more isolated. Set concurrent = 2 in /etc/gitlab-runner/config.toml on S-8.
Step 4: Drone or Woodpecker with docker compose
Drone and Woodpecker look the same from the outside: a server container plus an agent container that talks to the Docker socket. Woodpecker's compose file wants three things, the public hostname of the server, an OAuth application from your git host and a shared agent secret. Keep the secret in /opt/woodpecker/docker-compose.yml and start it with docker compose up -d. Put Traefik or Caddy in front for TLS on the server's hostname.
Step 5: staging stacks per client
One directory per client with its own compose file and .env, one reverse proxy for all. The last pipeline job pulls the new image and restarts the stack.
Step 6: keep the disk alive
Pruning belongs on a timer. If it depends on someone remembering, the disk fills, and it fills at night.
A weekly docker system prune with the volumes flag left off keeps the 120 GB free of layers nobody references any more. Boring, scheduled, and it has saved us more than one Saturday.
Pitfalls we see, and how to avoid them.
A runner with repo access on a shared box
A self-hosted runner executes whatever the workflow file tells it to. That includes a workflow from a pull request a stranger opened five minutes ago. So: no runners on public repositories where anyone can open a PR, and no runner on a server that also holds production data. CI on one box, staging on another. We have been asked for a cheaper layout more than once. We still say no.
The cache disk fills up quietly
Images, dangling layers, build caches, old artifacts. They pile up. The first sign is usually a job dying with "no space left on device" at 03:00. Prune on a schedule, step 6 covers it, put an alert on disk usage, and take S-8 or bigger. On S-4 the 60 GB goes fast.
Secrets that end up on disk
Deploy keys, registry logins and .env files written by one job stay on the runner for the next. Use the CI system's secret store, log out of registries at the end of the job, and prefer the Docker executor or ephemeral runners so the filesystem is fresh per job. Never bake a token into an image layer.
Long-lived runners drift
After a few thousand jobs a runner has global npm packages nobody installed on purpose, half a toolchain upgrade and a workspace directory that has never been cleaned. Ephemeral runners (--ephemeral on GitHub, the Docker executor on GitLab) reset that per job. For the host itself, unattended-upgrades and a reboot window keep the kernel current.
Pick the region closest to your team.
For the runner itself latency barely matters. For the staging URLs a client opens it does. From Amsterdam we measure 9.0 ms to London and 8.5 ms to Frankfurt. From New York it is 5 to 8 ms to Boston and 7 to 12 ms to Washington DC. Both regions cost the same. See EU VPS and USA VPS.
Questions about running CI runners on a VPS.
How many vCPUs does a CI runner need?
Two. That is why the S-4 is the smallest plan we suggest. On the S-8 you have three, and one runner then takes two jobs at once. Rust, Android and other compile-heavy work wants the four pinned cores of the D-8, for one reason: the build time stops drifting from run to run.
Can I run GitHub Actions and GitLab runners on the same VPS?
Yes, and it works better than you would expect. They are two separate services that ignore each other, and both can use the same Docker daemon, so they even share the layer cache. Watch the concurrency though. Two runners at two jobs each is four builds on three vCPUs, and then all four are slow.
Does Docker work on the VPS?
Yes. It is KVM with full root and your own kernel, so Docker just works, docker-in-docker works, and nested virtualisation for VM-based tests works too.
How do I create preview environments per branch?
The full version: the pipeline calls the REST API, creates a small VPS with cloud-init, deploys the branch, posts the URL in the pull request and deletes the server when the branch merges. Tidy, and honestly a lot of machinery for a preview. For most teams we would rather run one S-16 with a compose stack per branch behind a wildcard DNS record. Same result, fewer moving parts, one server to keep an eye on.
What about bandwidth for image pulls and artifacts?
Traffic is unlimited on every plan, so image pulls and artifact uploads do not cost anything extra. As for speed, we measured the public network from Amsterdam with iperf3 and got 9.40 Gbit/s sending and 17.1 Gbit/s receiving.
Is it safe to let a runner run pull requests from strangers?
No. We do not bend on this one. The runner executes whatever workflow the PR carries, so a stranger's PR is a stranger's code on your server. Self-hosted runners belong on private repositories with contributors you trust, on a VPS that holds no production data. That is the whole rule.
Deploy a measured VPS
From $6.79/mo on a 2-year term. KVM on AMD EPYC, NVMe RAID10, ECC memory. No setup fee, cancel monthly.
30-day performance guarantee. Not satisfied? Full refund, no admin fee. How it works