Skip to main content
Self-Hosting & Privacy

Self-Hosted GitHub Actions Runner at Home

A real config.sh/svc.sh walkthrough for a self-hosted GitHub Actions runner, an honest spec comparison against GitHub-hosted minutes, and why GitHub itself warns against running one on a public repo (fork PRs can execute code on it).

milanbuha00August 12, 20269 min read
ShareXin
Self-Hosted GitHub Actions Runner at Home

A standard GitHub-hosted Linux runner gives you 4 CPU cores, 16 GB of RAM, and 14 GB of disk β€” on a public repo. Push the same workflow to a private repo and it quietly drops to 2 cores and 8 GB, because private-repo minutes are metered and GitHub sizes the box accordingly. My Proxmox box hands a runner 8 cores and 32 GB without blinking, and it never wipes its Docker layer cache between jobs, because it's the same container every time.

That gap β€” plus a monthly minutes allowance that private repos can actually burn through β€” is why so many self-hosters end up pointing a workflow at their own hardware. It's a genuinely easy thing to set up. It's also the one CI decision where skipping the fine print can hand a stranger's pull request a shell on your machine.

TL;DR

  • A self-hosted runner is just a small agent you install on your own machine that polls GitHub for jobs β€” setup is config.sh + svc.sh install, about 10 minutes.
  • GitHub-hosted private-repo runners ship with half the CPU and RAM of the public-repo default (2 cores/8 GB vs 4 cores/16 GB), and private repos have a finite free-minutes allowance.
  • A homelab box typically offers more cores, more RAM, and persistent build caches that GitHub-hosted runners reset every run.
  • GitHub explicitly recommends against self-hosted runners on public repositories β€” anyone who opens a fork pull request can potentially execute code on your runner.
  • The fix isn't "don't self-host," it's knowing which settings (required approval, ephemeral runners, private-only) make it safe for your situation.

KEY-STAT: 2,000 minutes/month β€” GitHub Actions' free CI/CD minute allowance on private repos before per-minute charges apply (Free plan, 2026 pricing)

Why point a workflow at your own hardware

The reasons people move off GitHub-hosted runners aren't exotic β€” they're the same three or four things that come up in most self-hosting decisions.

Minutes cost money on private repos. Public repos get unlimited GitHub-hosted minutes; private repos don't. The Free plan currently includes 2,000 CI/CD minutes a month, and every OS multiplies against that allowance differently β€” Linux burns it at 1x, Windows at 2x, macOS at roughly 10x. A team running a matrix build across all three can chew through the free tier fast, and Linux overage currently runs a few tenths of a cent per minute, which adds up on a busy repo.

Persistent caches. GitHub-hosted runners are ephemeral by design β€” a fresh VM spins up per job and is destroyed after. That's great for isolation and terrible for anything with a slow first build: Docker layers, node_modules, Rust's target/, Go's module cache. A homelab runner keeps that cache warm between runs, so a rebuild that only touched one file can skip most of the work GitHub would redo from scratch every time.

Hardware access. Sometimes the job genuinely needs something GitHub's shared fleet doesn't offer as standard β€” a GPU for a training or transcoding step, ARM hardware to test against real silicon instead of emulation, or a specific accelerator. Self-hosting is the direct way to get that into CI.

Network access to internal resources. A runner sitting on your homelab's network can reach things a cloud-hosted runner never will without extra plumbing β€” an internal registry, a local database for integration tests, or a Proxmox API for deploy scripts, without opening anything to the public internet.

None of that requires exotic hardware. If you're weighing what to run a runner on in the first place, the same tradeoffs covered in our best mini PC for a home server guide apply directly β€” a runner is just another lightweight, always-on service.

Registering a self-hosted runner (the real steps)

This is GitHub's actual documented process, not a paraphrase. From your repository (or org) go to Settings β†’ Actions β†’ Runners β†’ New self-hosted runner, pick your OS, and GitHub will hand you a download link and a short-lived registration token generated specifically for that page visit β€” it expires in about an hour, so copy the real one shown to you rather than reusing an old command from a tutorial.

mkdir actions-runner && cd actions-runner

curl -o actions-runner-linux-x64.tar.gz -L \
  https://github.com/actions/runner/releases/download/vX.Y.Z/actions-runner-linux-x64-X.Y.Z.tar.gz
tar xzf ./actions-runner-linux-x64.tar.gz

./config.sh --url https://github.com/OWNER/REPO --token YOUR_REGISTRATION_TOKEN

./run.sh

The exact version and download URL GitHub gives you will differ β€” always copy the one shown on your repo's New runner page rather than hardcoding a version number. config.sh works the same way for org-level registration, just pointed at an org URL instead of a repo. Running ./run.sh in the foreground is enough to confirm the connection before you turn it into a service.

A successful registration ends with √ Connected to GitHub and Listening for Jobs. At that point the runner shows up under Settings β†’ Actions β†’ Runners as idle, and any workflow with runs-on: self-hosted (or a matching custom label) will route to it.

Running it as a service with svc.sh

Running ./run.sh in a terminal works for testing, but it dies the moment you close the session. For anything real, install it as a service so it survives reboots:

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

By default the runner is not ephemeral β€” the same container or VM handles job after job, keeping caches warm but also keeping any leftover state (and any leftover compromise) around for the next run. GitHub also supports an --ephemeral flag on config.sh that tears the runner down after a single job, trading the cache benefit for a clean slate every time. On a private repo with trusted collaborators, persistent is usually fine. On anything public, ephemeral is the safer default β€” more on why below.

GitHub-hosted vs a homelab box: the actual numbers

Here's the comparison as GitHub's own documentation and pricing pages state it, next to what a modest homelab box typically offers:

GitHub-hosted (public repo)GitHub-hosted (private repo)Typical homelab runner
CPU4 cores2 cores4–16+ cores, whatever you allocate
RAM16 GB8 GB8–64+ GB, whatever you allocate
Disk14 GB SSD14 GB SSDWhatever your storage pool has free
Build cacheReset every jobReset every jobPersists between jobs
CostFree, unlimited2,000 min/mo free (Free plan), then metered$0 marginal β€” hardware you already own
GPU/ARM accessNot on standard runnersNot on standard runnersWhatever hardware you pass through
IsolationFresh VM per jobFresh VM per jobDepends entirely on your config

Read that last row carefully β€” it's the whole tradeoff in one line. GitHub's isolation is automatic and non-negotiable; a homelab runner's isolation is a decision you have to make yourself, every time.

The security tradeoff you cannot skip

Warning

GitHub's own security documentation states plainly that self-hosted runners should almost never be used on public repositories, because anyone can open a pull request from a fork and potentially run arbitrary code on that runner. This isn't a theoretical edge case β€” it's the default behavior unless you explicitly lock it down, and it's the single biggest reason people get burned by self-hosted CI.

Why a fork PR is dangerous specifically

A workflow triggered by a pull request runs the code in that pull request β€” including its CI config. On a GitHub-hosted runner, that's contained: a throwaway VM with limited default permissions, destroyed the second the job ends. On a self-hosted runner, that same untrusted code executes on hardware you own, with network access to whatever else lives on that machine or network, and β€” depending on your workflow's permissions β€” potential access to the GITHUB_TOKEN and any repository secrets available to that trigger. If the runner is non-ephemeral, a malicious job can leave something behind for the next job to inherit, not just do damage during its own run.

This is exactly why our writeup on Docker vs. virtual machines matters here too β€” a runner in a lightly-isolated container is a very different blast radius than one in a VM you're comfortable throwing away.

Making it safe if you stay on a public repo

Tip

If you need a self-hosted runner on a repo that has to stay public, three settings do most of the work: require approval for all outside collaborators before their workflow runs (Settings β†’ Actions β†’ General), use --ephemeral runners so no job inherits state from the last one, and scope the runner to a dedicated, disposable environment rather than your daily-driver box. None of these make it "GitHub-hosted safe," but together they close the door fork PRs walk through by default.

The honest, simplest fix for most homelab use cases is narrower than any of those settings: keep self-hosted runners on private repos only. That single choice removes the fork-PR attack path entirely, since only people you've already granted write access can open PRs against a private repo in the first place. If you're self-hosting for cost or speed reasons on your own personal or team projects, that's almost always where the runner belongs.

Where the runner sits in a Proxmox homelab

In practice, a GitHub Actions runner is a lightweight, mostly-idle service β€” it's not competing for resources with anything demanding. Dropping one into a Docker container or LXC on an existing Proxmox box, alongside whatever else you're already running, is enough for most personal CI workloads; there's rarely a reason to dedicate hardware to it. If you're still deciding where new containers like this should live relative to VMs, that's exactly the question our Proxmox vs. Docker guide walks through, and if you're setting up the underlying homelab for the first time, our beginner's homelab guide is the starting point.

Note

Treat a self-hosted runner's host like any other service with network access and secrets flowing through it β€” patch it, keep it off the public internet directly, and don't reuse the same box for anything you'd hate to see compromised.

Keep the runner's job narrow, keep it off public repos unless you've deliberately locked it down, and it's one of the lower-maintenance things you can add to a homelab β€” a real speed and cost win for the risk it's actually taking on.

FAQ

Is it safe to use a self-hosted GitHub Actions runner on a public repository?

Not by default. GitHub's own documentation recommends against it, because any fork pull request can potentially execute code on the runner. It's only reasonably safe with additional controls in place β€” required approval for outside collaborators, ephemeral runners, and a disposable, isolated host.

How do I set up a self-hosted GitHub Actions runner?

From your repo's Settings β†’ Actions β†’ Runners, choose "New self-hosted runner," download the package GitHub links you, run ./config.sh --url <repo-url> --token <token> with the token shown on that page, then either run ./run.sh directly or install it as a background service with sudo ./svc.sh install && sudo ./svc.sh start.

How many free GitHub Actions minutes do I get on a private repo?

On the Free plan, private repos currently get 2,000 CI/CD minutes a month before per-minute charges apply, with Windows and macOS jobs consuming that allowance faster than Linux jobs. Public repos get unlimited GitHub-hosted minutes regardless of plan.

What hardware specs do GitHub-hosted runners actually use?

The standard Linux and Windows runners ship with 4 CPU cores, 16 GB of RAM, and 14 GB of SSD storage on public repos β€” private-repo runners on the same OS drop to 2 cores and 8 GB. A homelab machine will typically match or exceed those numbers, and won't reset its cache between jobs.

Can a self-hosted runner run as an ephemeral, one-job-then-destroy instance?

Yes. Passing --ephemeral to config.sh configures the runner to accept exactly one job and then deregister, so nothing persists between runs. It sacrifices the persistent-cache benefit that draws a lot of people to self-hosting in the first place, but it closes off the main way a compromised job can affect the next one β€” a reasonable trade on anything public-facing.

Related stories

More from Self-Hosting & Privacy

Stay in the loop

Get the latest articles delivered to your inbox. No spam, unsubscribe anytime.

Read next

Self-Hosted LLM at Home: What Actually Runs

A ChatGPT price hike or a moment of regret pasting something sensitive into a public chatbot raises the obvious question: can your homelab box just run the AI itself? Real tokens-per-second numbers across Raspberry Pi 5, N100 mini PC, and GPU tiers -- plus what a local LLM can and can't replace.

Continue Reading