Hackorda Docs
Ops

Self Hosted Runner

Operational guide for moving Hackorda's CI/CD off GitHub-hosted runners onto a self-hosted runner — fixed-cost compute instead of per-minute billing.

When to do this

GitHub-hosted runners bill per minute (Linux $0.006/min after the 2,000-minute free tier). That's fine for low, spiky usage. Switch to a self-hosted runner when usage becomes high and sustained — the inflection point is roughly when monthly overage would exceed the cost of a small always-on VM.

Monthly CI usageGitHub-hosted costSelf-hosted (one VM)
≤ 2,000 min$0 (free tier)not worth it
~5,000 min~$18/mo, variable~$24/mo fixed — break-even
~20,000 min (10×)~$110/mo, variable, ceiling-prone~$24–48/mo fixed, no ceiling

Self-hosted runners currently incur no per-minute charge (a charge was announced for 2026 then postponed — re-check before relying on it long-term). They're also faster: caches live on local disk, no cold runner spin-up.

Security model

A self-hosted runner executes whatever a workflow tells it to. Rules:

  • Private repo only. Never attach a self-hosted runner to a public repo — any fork PR could run arbitrary code on your box. Hackorda's repo is private, so all PRs are trusted.
  • Do NOT run the runner on the production droplet. A compromised or buggy build must not have a foothold next to prod. Use a separate, dedicated build VM.
  • Give the runner box only the access it needs: Docker, outbound network, and the deploy SSH key (already a GitHub secret — the workflow injects it; nothing extra to store on the runner).

Provisioning the build VM

A DigitalOcean droplet is the natural fit (you already deploy there).

  • Size: 4 GB / 2 vCPU (s-2vcpu-4gb, $24/mo) is comfortable for the Next.js Docker build. 8 GB ($48/mo) if builds feel slow or you want headroom for parallel jobs.
  • OS: Ubuntu 24.04 LTS.
  • Region: same as the prod droplet (faster image pull on deploy).

Install prerequisites on the box:

# Docker (the deploy job builds + pushes an image)
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker "$USER"   # re-login after this

# Node 22 (the check/build jobs)
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs git

Registering the runner

  1. GitHub → repo Settings → Actions → Runners → New self-hosted runner → Linux x64. GitHub shows a ./config.sh command with a short-lived registration token.
  2. On the build VM, run the download + ./config.sh block GitHub gave you. When prompted for labels, add a custom one — e.g. hackorda-build — so workflows can target this runner explicitly.
  3. Install it as a service so it survives reboots and runs unattended:
sudo ./svc.sh install
sudo ./svc.sh start
sudo ./svc.sh status   # verify it's listening

The runner now appears under Settings → Actions → Runners as Idle.

Wiring the workflow

In .github/workflows/workflows.yml, change runs-on for the jobs you want self-hosted. Recommended split:

  • deploy → self-hosted. It's the heaviest job (full Docker image build) and the biggest minute consumer. This is the highest-leverage move.
  • check → leave on ubuntu-latest. It's light (lint + typecheck), the 2,000 free minutes cover it easily, and GitHub-hosted keeps PR feedback fast and parallel.
  • build (PR-only full build) → either; move it self-hosted too if PR builds are eating the budget.
  deploy:
    runs-on: [self-hosted, hackorda-build]
    # ...rest unchanged

One self-hosted runner processes jobs serially. For a small team that's fine. If PR check/build and a deploy start queueing behind each other, either add a second runner (repeat the registration steps) or move only deploy self-hosted and leave the rest GitHub-hosted.

Maintenance

  • Updates: the runner agent auto-updates by default. Keep the OS patched (unattended-upgrades).
  • Disk: Docker layer cache grows. Add a weekly docker system prune -af --filter "until=168h" cron so the disk doesn't fill.
  • If the runner is offline: jobs targeting self-hosted queue (they don't fail) until it's back, or until the job timeout. Monitor the runner's status; consider a second runner for redundancy if deploys become business-critical.
  • Decommissioning: sudo ./svc.sh stop && sudo ./svc.sh uninstall, then remove the runner in GitHub Settings before destroying the VM.

Cost summary

ItemCost
Build VM (DO s-2vcpu-4gb)~$24/mo fixed
GitHub-hosted minutes still used (light check job)within the free 2,000/mo
Per-minute CI overage$0

A predictable ~$24/mo flat line with no minutes ceiling, replacing a variable bill that hard-stops the whole pipeline when the spending limit is hit.

On this page