By ProDevs Team · Published 2026-09-27
Drafted with AI assistance, fact-checked and edited by the ProDevs team
Who this is for: Developers and DevOps/SRE engineers · Intermediate (comfortable with a Linux shell, systemd and GitHub Actions YAML) · Small teams with private repos and one always-on Linux x64 box. Not for: public-repo-only projects or large macOS fleets. You’ll need: a Linux x64 machine, admin access to the repo, outbound HTTPS on port 443 and sudo on the box.
Is one self-hosted GitHub Actions runner cheaper than paying for hosted minutes? Start from the command that creates one, ./config.sh --url <org-or-repo-url> --token <registration-token> --labels nuc,prodevs, run on a box you already own. Below: our own numbers from 16 to 27 Sep 2026, the setup, and what broke.
Short answer: Self-hosted GitHub runner minutes cost $0 from GitHub as of 27 September 2026. A $0.002-per-minute fee announced on 16 December 2025 was postponed with no new date and is not in effect. You pay for the machine, its power and your maintenance time. Self-hosting pays off when your hosted-minute bill is bigger than those three combined.
- Goal: one self-hosted runner, installed as a systemd service, running private-repo jobs.
- Prerequisites: the four items under Prerequisites below, per GitHub’s runner requirements.
- Time needed: about 5 min 40 s from first listener start to first green job, read from the runner’s
_diaglogs. Our box already had Ubuntu and Docker, so blank-box time is unknown. - Success criterion: a job with
runs-on: [self-hosted, nuc, prodevs]goes green and the runner returns to Idle (runner statuses). - Tested on:
actions/runnerv2.337.0 (26 Aug 2026), Ubuntu 24.04.4 LTS (kernel 6.8.0), Docker Engine 29.5.0, auto-update on.
Does GitHub charge for self-hosted runners in 2026?
GitHub’s Actions billing doc says usage is free for self-hosted runners, and for public repos on standard GitHub-hosted runners (checked 27 Sep 2026). The runner pricing reference lists no self-hosted line.
Fee status as of 27 Sep 2026. On 16 Dec 2025 GitHub announced a $0.002-per-minute “Actions cloud platform charge” on self-hosted usage, due 1 Mar 2026. Within days GitHub postponed it “to re-evaluate our approach”. The fee is postponed, not in effect, and GitHub has published no new date or rate. The roadmap item was closed as “not planned”, but GitHub’s word is “postponing”, so re-check the changelog before budgeting.
GitHub instead cut hosted prices by up to 39% on 1 Jan 2026. Linux 2-core x64 is $0.006 per minute, a rate GitHub says already includes the $0.002 platform charge. Included minutes cover standard hosted runners only. Larger runners are billed from the first minute, even on public repos.
What does a self-hosted runner actually cost?
How is the hosted bill worked out?
hosted bill = (monthly standard-runner minutes - included minutes) x per-minute rate
+ (larger-runner minutes x larger-runner rate)
GitHub rounds each job up to a whole minute. Included minutes, from the billing doc (checked 27 Sep 2026):
| Plan | Included minutes / month (standard runners, private repos) |
|---|---|
| GitHub Free (personal) / Free for organizations | 2,000 |
| GitHub Pro / GitHub Team | 3,000 |
| GitHub Enterprise Cloud | 50,000 |
Rounding hurts short jobs. Ours came to 3,006.3 raw minutes but 3,612 billed, about 20% more. Our median job took about 2.5 minutes. Since 1 Jun 2026, Copilot code review also uses Actions minutes on private repos.
What goes into the self-hosted side?
self-hosted cost = hardware price / months of expected life
+ (kWh used) x tariff, where kWh = watts x hours / 1000
+ extra network cost, if any
+ maintenance hours x what an hour of your time is worth
Our numbers for 2026-09-16 to 2026-09-27 (10.85 days, every org repo on the runner). The hosted-equivalent line prices the same jobs at $0.006 per minute:
| Line | Our self-hosted runner box (Intel mini PC) | Basis |
|---|---|---|
| Jobs run | 1,249 | GitHub API job records, matched to the runner’s own logs |
| Hosted-equivalent cost | $21.67 (3,612 billed min) | Measured minutes x $0.006 |
| Hardware | ₹0 marginal (ESTIMATE) | We already owned the box, which runs 24/7 for other home-lab services. Nothing amortised |
| Power | about ₹72 per 30 days (ESTIMATE, assuming ₹8/kWh, 25 W idle, 90 W under CI load) | 50.11 busy hours x 65 W extra = about 3.26 kWh in 10.85 days. Not measured, no meter |
| Maintenance time | Unknown | We didn’t log it |
Caveats: the first week was migration-heavy, so scaling $21.67 to a month (about $59.92, an ESTIMATE) overstates it. Hosted 2-core runners are probably slower than our 24 cores (unmeasured). Charging the box’s full idle draw to CI makes power about ₹144 a month (ESTIMATE, same assumptions).
Where does self-hosting lose?
- One runner is one queue. Across all 1,249 jobs, the median wait from created to started was 228 s, the max about 61 minutes, and 226 jobs waited over 10 minutes. GitHub-hosted runners autoscale. One box doesn’t.
- Idle hardware. A box bought only for CI draws power all month, while hosted runners bill only job minutes, rounded up per job.
- macOS and on-call. A Linux box can’t run macOS jobs (hosted macOS is $0.062/min). When the box misbehaves, you fix it.
Why did ProDevs move CI to a home server?
This section is ProDevs’ own experience on our home-lab runner. It isn’t a general statement about GitHub.
From 23:46 IST on 15 Sep 2026, our hosted jobs stopped starting. Each blocked job carried this annotation:
The job was not started because recent account payments have failed or your spending limit needs to be increased. Please check the 'Billing & plans' section in your settings
The message covers both payment and limit, so we don’t know which. We were on GitHub Free. We had used 2,020 minutes in August and 2,025 in September against 2,000 included, with $0 net billed. In our three main repos, 36 jobs failed without starting. The news app repo had no CI for about 10 h 19 min. We already had a home lab in India with a small home server, a gaming mini PC (24 cores, 91 GiB RAM, two NVMe SSDs) running 24/7, so we moved CI there. The first green job on it was at 10:05 IST on 16 Sep.
Our home-server runner kept running jobs while hosted jobs in the same org stayed blocked. Two unmigrated repos kept hitting the block (68 and 8 jobs), while none of the 1,249 jobs on the runner box failed to start. That is our data only. GitHub doesn’t document what a billing lock does to self-hosted jobs.
Prerequisites: what do you need before setup?
- A Linux x64 machine on a supported OS (Ubuntu 20.04+ or Debian 10+).
- Admin access to the repo (or org owner, for an org-level runner).
- Outbound HTTPS on port 443.
sudoon the box to install the service.
How do I set up a self-hosted runner as a service?
We run a persistent runner as a systemd service, at org scope.
Step 1: Register the runner
As the job user, download v2.337.0 and check the SHA-256 from the release notes:
mkdir -p ~/actions-runner && cd ~/actions-runner
curl -fsSL -o actions-runner-linux-x64-2.337.0.tar.gz \
https://github.com/actions/runner/releases/download/v2.337.0/actions-runner-linux-x64-2.337.0.tar.gz
sha256sum -c <<< "70920811a4f8ad4328818682bca5c6469c1c942fab52448868071d0063816613 actions-runner-linux-x64-2.337.0.tar.gz"
tar xzf actions-runner-linux-x64-2.337.0.tar.gz
Get a registration token from Settings → Actions → Runners → New self-hosted runner. It expires after one hour. Keep it out of chat and logs.
./config.sh --unattended --url <org-or-repo-url> --token <registration-token> \
--name <runner-name> --labels nuc,prodevs --work _work --replace
GitHub’s docs show --url, --token and --labels. The other four flags are defined in the runner’s source at v2.337.0: --unattended skips prompts, --name sets the runner name, --work sets the job folder, and --replace replaces an existing runner with the same name.
Check the --url. We once registered a second runner to a personal repo instead of the org, and it added no org capacity.
GitHub adds self-hosted, linux and x64 automatically. We route on custom labels only. Our first migration commit also put linux in runs-on. With that label set, 3 jobs were skipped or cancelled until a follow-up commit removed it.
Step 2: Run it as a service
Per GitHub’s service doc:
sudo ./svc.sh install <runner-user>
sudo ./svc.sh start
Then delete any saved copy of the token. Our generated unit has Restart=no. We’d add Restart=always via the service doc’s method, not by editing the unit file.
Step 3: Point a workflow at it
# .github/workflows/ci.yml
name: ci
on: [push]
jobs:
build:
runs-on: [self-hosted, nuc, prodevs]
steps:
- uses: actions/checkout@v7 # v7.0.1 was current on 27 Sep 2026: github.com/actions/checkout/releases
- run: make test
Expect environment fixes. One of our repos needed PYTHONPATH: ${{ github.workspace }}, which the hosted image had hidden.
Check it worked
cat ~/actions-runner/.service # prints the exact service name
systemctl is-active "$(cat ~/actions-runner/.service)"
grep -a "Listening for Jobs" ~/actions-runner/_diag/Runner_*.log | tail -1
Expect active, a recent Listening for Jobs line, and Idle under Settings → Actions → Runners.
From our runner’s _diag logs on 27 Sep 2026, sanitised (times in UTC, so this job ran 10:58:36 to 10:58:54 IST):
Current runner version: '2.337.0'
2026-09-26 20:26:20Z: Listening for Jobs
2026-09-27 05:28:36Z: Running job: lint-and-test
2026-09-27 05:28:54Z: Job lint-and-test completed with result: Succeeded
The same job in the worker log:
[2026-09-27 05:28:39Z INFO StepsRunner] Processing step: DisplayName='Checkout'
...
[2026-09-27 05:28:40Z INFO StepsRunner] Processing step: DisplayName='PHP Lint'
...
[2026-09-27 05:28:45Z INFO StepsRunner] Processing step: DisplayName='Run unit tests (1 of 4)'
...
[2026-09-27 05:28:46Z INFO StepsRunner] Processing step: DisplayName='Run unit tests (4 of 4)'
...
[2026-09-27 05:28:46Z INFO StepsRunner] Processing step: DisplayName='Post Checkout'
...
[2026-09-27 05:28:46Z INFO JobRunner] Job result after all job steps finish: Succeeded
What about ephemeral runners?
Ours ran one green job as an ephemeral runner (--ephemeral: one job, then it deregisters) on 16 Sep. GitHub recommends them for autoscaling. We went persistent the same morning, because each re-registration needs a fresh org token that our automation account can’t mint. With a GitHub App or admin identity, JIT runners are cleaner.
How do I keep a self-hosted runner secure?
GitHub recommends self-hosted runners only for private repos, because a fork’s pull request can run code on your machine. The hardening guide says “almost never” for public repos, and warns that destroying a runner after each job “might not be as effective as intended”.
Our setup, weak spots and all:
- Private repos only. All five repos using the runner are private, as GitHub advises.
- The runner user is effectively root. It’s in the
dockergroup, which is effectively root on the host. Itssudoneeds a password, so jobs can’t use it non-interactively. - No cleanup between jobs. The runner is persistent, so workspace and Docker state carry over (GitHub’s hardening guide warns of persistent compromise). On 22 Sep a frontend build failed on a Docker overlay2 rename race on this shared host, then passed on re-run.
- Network and secrets: outbound HTTPS only (the runner’s requirement), no inbound ports. Secrets arrive only as Actions secrets.
- Auto-update is on. With
--disableupdate, you must update within 30 days of each release or GitHub stops queuing jobs.
Warning: destructive step. These commands stop the service and permanently deregister the runner. Jobs queued for its labels will wait up to 24 hours, then fail. Get a remove token from the runner’s Remove option in Settings first.
sudo ./svc.sh stop && sudo ./svc.sh uninstall
./config.sh remove --token <remove-token>
Troubleshooting: what breaks on a home-server runner?
Ours, 16 to 27 Sep 2026:
| Incident | Symptom | Fix |
|---|---|---|
| Hung listener (26 Sep) | Job sat Queued while systemd said active. The log went silent after a job-broker read error |
kill -9 only Runner.Listener, and the service wrapper starts a new one. Silent 18 min 50 s, fix-to-green 2 min 40 s |
| Single-runner backlog (17 Sep) | 120 jobs, max queue wait about 22 min | Runner stayed busy and drained it. A second runner is the real fix |
| Docker overlay2 race (22 Sep) | “Build frontend” failed on the shared host | Re-dispatch went green |
| Host reboots (22 to 23 Sep) | Three boots, an Ethernet driver watchdog timeout before one | Service active again a minute after the last boot. Cause not established |
| Disk growth | 749.2 GB Docker images (555.9 GB reclaimable), 425.8 GB build cache on 27 Sep | Not all from CI. Scheduled pruning planned |
Lesson from 26 Sep: systemd active doesn’t mean listening. Check the newest _diag/Runner_*.log is fresh:
ls -t ~/actions-runner/_diag/Runner_*.log | head -1 | xargs ls -l
Also plan for GitHub’s 2026 version enforcement (see FAQ). A runner offline over 14 days is removed automatically.
Should you self-host your runners?
- Self-host if your private-repo builds are Linux, you already own an always-on box, and someone will watch it (GitHub’s add-runner guide). Add a second runner once queue waits hurt.
- Stay on hosted minutes if you fit inside your included minutes, need wide parallelism, or build only public repos on standard runners.
- Use Actions Runner Controller if you run Kubernetes and have the expertise. GitHub supports only the Autoscaling Runner Scale Sets mode. Legacy ARC is community-maintained.
- Consider larger hosted runners (always billed) or a managed runner vendor for more CPU per job without owning hardware.
Related: if the runner box also hosts your apps, read Docker or a VPS for that choice, and Docker volume vs bind mount before you mount build caches into job containers.
Talk to ProDevs if you want help setting up a runner like this.
FAQ
Are self-hosted GitHub Actions runners free?
GitHub charges no per-minute fee for them as of 27 Sep 2026. You pay for hardware, power and your time. The $0.002-per-minute fee announced on 16 Dec 2025 was postponed with no new date and is not in effect.
Can I use a self-hosted runner on a public repository?
GitHub advises against it, since forks can run code on your machine. Standard hosted runners are free on public repos, but larger runners are always billed.
What happens if my runner is out of date?
Runners below v2.329.0 can’t register. With auto-update off, you must install each release within 30 days or GitHub stops queuing jobs. A critical security update blocks jobs until you install it. GitHub lists 25 Sep 2026 as full enforcement for Enterprise Cloud. It says the change applies to github.com but gives no separate date for Free, Pro or Team.
Why did my GitHub Actions job say “recent account payments have failed”?
We saw that text on every blocked job, but GitHub’s docs don’t publish it. Documented causes: an exhausted budget set to “Stop usage when budget limit is reached” (Budgets and alerts), a past-due payment that locks paid features (locked account), or an Indian-issued card declined for recurring charges under RBI rules (payments in India). “Spending limit” is budgets’ older name.
How many free minutes does GitHub include?
Per month, for standard runners on private repos: 2,000 on Free, 3,000 on Pro and Team, and 50,000 on Enterprise Cloud (checked 27 Sep 2026). Larger runners can’t use them.


