I switched all my GitHub Actions to a self-hosted runner. 4m 12s → 38s, $0/month.

Self-hosted GitHub Actions runner workstation at night

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.
Stopwatch on a desk next to a mechanical keyboard

Why it’s faster

  1. No VM cold start. GitHub-hosted spends 20–40s booting Ubuntu before your job sees a shell. My runner is already running.
  2. Caches stay hot between runs. A cold npm ci takes 47s on a typical repo of mine. Warm reuse takes 4s. Same wins for pip wheels, Docker layers, and the Cargo registry. No actions/cache dance.
  3. 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.
  4. 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.

Hands typing on a mechanical keyboard with code on monitors

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.

Leave a Reply

Your email address will not be published. Required fields are marked *