
I moved every workflow to a self-hosted GitHub Actions runner on my desktop. CI got 6.6x faster. My GitHub bill went to $0.
The numbers
- GitHub-hosted average: 4m 12s per workflow.
- Self-hosted on my Ryzen 9 7950X: 38s per workflow.
- Monthly minutes used before: ~3,400 across 8 repos. Free tier is 2,000.
- Monthly cost now: $0.

Why it’s faster
- No VM cold start. GitHub-hosted spends 20–40s booting Ubuntu before your job sees a shell. My runner is already running.
- Caches stay hot between runs. A cold
npm citakes 47s on a typical repo of mine. Warm reuse takes 4s. Same wins for pip wheels, Docker layers, and the Cargo registry. Noactions/cachedance. - 16 cores instead of 4. GitHub-hosted Linux is 4 vCPU, 16 GB RAM, 14 GB SSD. My desktop is 16 cores, 64 GB, 2 TB NVMe.
- Local resources. Pulling a private container is a LAN hop, not a 200ms round trip to ghcr.io. Tests can hit my local Postgres directly — no service containers in the workflow.
Setup
Repo → Settings → Actions → Runners → New self-hosted runner. GitHub gives you the registration token. Run:
mkdir actions-runner && cd actions-runner
curl -o runner.tar.gz -L https://github.com/actions/runner/releases/download/v2.319.1/actions-runner-linux-x64-2.319.1.tar.gz
tar xzf runner.tar.gz
./config.sh --url https://github.com/USER/REPO --token YOUR_TOKEN
sudo ./svc.sh install
sudo ./svc.sh start
Done. Systemd service. Survives reboots. Auto-updates on minor versions.
Workflow change
Swap one line in .github/workflows/*.yml:
jobs:
build:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test
That’s the migration.

What I did NOT lose
- Matrix builds. Still work. They serialize if you only have one runner.
- Secrets. Same
${{ secrets.X }}API. - UI. Logs, badges, re-runs, annotations — unchanged.
- actions/* marketplace. Anything that runs in bash or a Linux container still runs.
What to watch
Never use self-hosted runners on public repos. A pull request from a fork can execute arbitrary code on your machine. Private repos only.
Disk fills up. _work/ accumulates checkouts forever. Weekly cron:
0 3 * * 0 find ~/actions-runner/_work -mindepth 1 -maxdepth 1 -type d -mtime +7 -exec rm -rf {} +
One runner = serialized jobs. If a workflow holds the runner, the next workflow queues. Install a second runner in a different folder when you need parallelism:
cp -r actions-runner actions-runner-2
cd actions-runner-2 && ./config.sh --url ... --token ... --name runner-2
sudo ./svc.sh install && sudo ./svc.sh start
Single point of failure. If my desktop is off, CI is off. Fine for me. Not fine for a team.
When NOT to do this
If you’re under 2,000 free minutes and your jobs are CPU-light, GitHub-hosted is simpler. Self-hosted wins when you’re over the cap, doing GPU work, hitting the 6-hour job timeout, or your jobs are bottlenecked on cold caches and fresh VMs.
My setup, exactly
- Ryzen 9 7950X, 64 GB DDR5, 2 TB NVMe, RTX 4090.
- Ubuntu 24.04, runner v2.319.1, systemd unit auto-starts on boot.
- 8 private repos pointed at one runner. Never seen queue contention.