EQVPS

Running a Self-Hosted GitHub Actions Runner on a VPS

Jul 27, 2026 · 4 min read · EQVPS Team

GitHub-hosted runners are a fine default. You stop needing them the moment a build wants something they don't have — your private package registry, a specific toolchain version you're tired of reinstalling every run, a database on your own network, or just more control over the machine. That's when a self-hosted runner on a VPS earns its keep.

This guide gets one running properly: installed, registered, alive under systemd, and — the part people get wrong — safe. Let's start with that last one, because it's the part that bites.

The one security rule

A GitHub Actions workflow runs arbitrary code — whatever is in the workflow file, and whatever that code pulls in. On your own repo that's your code, and it's fine. On a public repo, a pull request from a stranger can run their code on your runner. That's not a bug; it's how CI works. GitHub's own documentation says plainly: don't use self-hosted runners with public repositories.

So the rule is simple and non-negotiable: self-hosted runners are for private repos. If your repo is public, use GitHub-hosted runners and move on. Everything below assumes a private repo.

What a runner needs from the box

It depends on the build, and you should size to yours rather than a number off a page:

Disk matters too: build caches, Docker layers, and cloned repos add up. Keep an eye on it and prune.

1. Prepare the box

Create a non-root user for the runner — GitHub's installer refuses to run as root anyway, and you want it that way:

sudo adduser --disabled-password --gecos "" runner
sudo usermod -aG sudo runner   # only if your builds genuinely need sudo

Install whatever your builds need — a language toolchain, Docker, build tools. For example, if your jobs build containers:

sudo apt update && sudo apt install -y docker.io
sudo usermod -aG docker runner

2. Download and register the runner

In your repo (or org) on GitHub, go to Settings → Actions → Runners → New self-hosted runner. GitHub gives you the exact download commands and a registration token (it's short-lived — grab it fresh). As the runner user:

sudo -iu runner
mkdir actions-runner && cd actions-runner
# use the exact URL GitHub shows you for your OS/arch:
curl -o actions-runner-linux-x64.tar.gz -L "URL_FROM_GITHUB"
tar xzf actions-runner-linux-x64.tar.gz
./config.sh --url https://github.com/YOUR_ORG/YOUR_REPO --token YOUR_TOKEN

config.sh asks for a runner name, labels, and a work folder — the defaults are fine to start. Labels are how your workflow targets this runner (runs-on: self-hosted).

3. Run it as a systemd service

The runner ships with a helper that installs a systemd service for you — use it, so the runner survives reboots and restarts on failure. Still as the runner user's install, but the service commands need root:

sudo ./svc.sh install runner
sudo ./svc.sh start
sudo ./svc.sh status

That registers actions.runner.* as a systemd unit running as the runner user, starting on boot. Logs go to journald:

sudo journalctl -u 'actions.runner.*' -f

Back in GitHub's Runners page, your runner now shows Idle — green dot. Point a workflow at it:

jobs:
  build:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4
      - run: make test

Push, and the job runs on your box.

4. Keep it clean

A self-hosted runner reuses its filesystem between jobs — that's the speed win (warm caches) and the footgun (leftover state). Two habits keep it healthy:

If you need a truly clean environment per job, run each job inside a container step — the runner stays, the job's mess doesn't.

When to self-host, honestly

Self-host when you need your own toolchain baked in, access to private network resources, or control over the machine. Stay on GitHub-hosted runners when a clean, throwaway environment and their minutes suit you — that's genuinely simpler, and simpler is worth something. And never, on a public repo, self-host. That one isn't a preference.

If a private-repo runner is what you need: pick a planSmall ($8) for lighter builds, Medium ($12) when they get heavier — pay in USDC or USDT (no KYC, no documents), and you'll have root in about 60 seconds. Then work down this page and you'll have a runner picking up jobs a few minutes later.

FAQ

Why run my own runner instead of GitHub-hosted?

Three real reasons: your own dependencies and toolchain baked in (no reinstalling them every run), access to private resources like an internal registry or database, and control over the machine — its size, its caches, its network. If GitHub-hosted minutes and a clean environment suit you, stay on them. Self-host when you specifically need one of those three.

Is it safe to use a self-hosted runner?

On a private repo, yes. On a public repo, no — never. A workflow runs arbitrary code from whoever triggers it, and on a public repo a stranger's pull request can run their code on your runner. GitHub's own docs say the same. Keep self-hosted runners to private repos, or accept that you're handing your box to the internet.

How much CPU and RAM does a runner need?

Depends entirely on your build. A typical compile-and-test job is comfortable with 2 cores and 4-8 GB — Small or Medium here. Heavy builds (large native compiles, big Docker images, memory-hungry test suites) want more, and you should size to your actual job, not a guess. Watch one real run and you'll know.

Can one runner handle multiple repos?

A runner can be registered to an organization and picked up by several repos, one job at a time by default. For more parallelism, run more runners — each is its own systemd service. Just keep them all on private repos.

Do I need to give you an ID?

No. Email to sign up, USDC or USDT to pay. No documents, root in about a minute.

← Back to blogSee plans & pricing →