I set up a GitHub self-hosted runner inside the homelab Kubernetes cluster. Here’s how it went.
With runs-on: github-runner, workflows can reach any Service inside the cluster without exposing the homelab publicly. That said, there were a few gotchas that cost me some time.
Step 0: Register a GitHub App
ARC works with a PAT too. I went with a GitHub App — adding and removing the App installation felt like a slightly cleaner way to manage access.
Go to org Settings → Developer settings → GitHub Apps → New GitHub App. The only permissions needed:
| Permission | Level |
|---|---|
| Repository → Actions | Read-only |
| Organization → Self-hosted runners | Read and write |
| Webhooks | Disabled (not needed) |
ARC v2 (scale-set mode) fetches jobs via polling, not webhooks, so webhooks can be left off.
After creating the app and installing it on the org, note down the App ID, Installation ID (visible in the install URL: github.com/organizations/<org>/settings/installations/<id>), and the private key (.pem).
Those go into a K8s Secret:
# github-runner-secretsgithub_app_id: "<App ID>"github_app_installation_id: "<Installation ID>"github_app_private_key: | -----BEGIN RSA PRIVATE KEY----- ... -----END RSA PRIVATE KEY-----RBAC Issues and Why Debugging Is Painful
Self-hosted runners run as ephemeral runners — the Pod is deleted as soon as the job finishes, success or failure. Logs disappear with it. This makes debugging genuinely painful.
The workflow becomes: open kubectl logs -f on the runner Pod, then trigger the job, and hope you catch the output before it’s gone. Not having a proper logging stack makes this worse than it needs to be.
Permission problems were the most common source of failures. When something is wrong with RBAC, the runner log shows errors like forbidden: attempt to grant extra privileges. Check there first. Combined with logs disappearing immediately, it took longer than it should have to connect the error to its cause.
The specific ArgoCD issue: ARC’s Helm chart auto-creates ServiceAccounts and Roles for its controller. ArgoCD treats these as unmanaged resources and prunes them on every sync. The listener Pod crashes at startup without RBAC, and since the logs disappear, the failure mode is hard to diagnose.
The fix is declaring those RBAC resources explicitly in the chart templates so ArgoCD owns them. Two ServiceAccounts — one for the listener pod, one for runner pods — and the pruning stops.
ARC Configuration
ARC (Actions Runner Controller) is a GitHub-maintained Kubernetes Operator that runs Actions runners as K8s Pods. The v2 scale-set mode starts a Pod only when a job is queued and removes it when done — no idle Pods by default.
gha-runner-scale-set: githubConfigUrl: "https://github.com/otama-homelab" minRunners: 0 maxRunners: 5 containerMode: type: "kubernetes-novolume"githubConfigUrl is set at org level so all repos under otama-homelab share one runner pool. Per-repo registration would require re-registering every time a new repo is added.
Why Self-Hosted in the First Place
Requests from GitHub — whether from hosted runners or webhooks — can’t reach internal homelab Services that aren’t publicly exposed. There are more situations where you want this than you’d expect: ArgoCD triggers, internal API calls, deploying to targets that are never meant to be public.
Runner Pods are normal K8s Pods, so they resolve internal DNS and reach any Service directly. On the workflow side, the only change is runs-on: github-runner.
Hopefully useful for anyone running into the same setup.









