How to Add a Self-Hosted Runner to a Repo
This guide adds an ARC (Actions Runner Controller) self-hosted runner scale set
on the Hetzner k3s cluster for a hungryhub-team repository, so its GitHub
Actions workflows can use runs-on: arc-runner-set-<name>.
Prerequisites:
kubectlaccess to the k3s cluster — How to Access the Hetzner k3s Cluster.helm,aws(SSO profileprod),python3.- Authentication is via GitHub Apps (not a PAT) — background in Hetzner k3s Self-Hosted Runners §0.
All paths below are under
hungryhub-terraform/github-runners/.
Decide: reuse an existing App, or create a new one?
Each runner authenticates as a GitHub App. There are four, grouped by repo:
| App | Repos | AWS secret |
|---|---|---|
hh-arc-server | hh-server | /github-runners/arc/hh-arc-server |
hh-arc-frontend | hh-pegasus, book-bite | /github-runners/arc/hh-arc-frontend |
hh-arc-platform | hungryhub-terraform, hungryhub-ai, hh-relay | /github-runners/arc/hh-arc-platform |
hh-arc-vendor | hhvendor-automation, hh-felidae | /github-runners/arc/hh-arc-vendor |
- Repo fits an existing group → reuse that App. Just install the App on the new repo (one UI click) and skip to step 2.
- New group / wants isolation → create a new App (step 1).
1. (Only if creating a new App) Provision a GitHub App
Run from github-runners/github-app-provisioning/. You must be an owner of
the hungryhub-team org (the create + install clicks happen as you in the
browser). Full runbook:
github-app-provisioning/README.md.
cd hungryhub-terraform/github-runners/github-app-provisioning
# Register the App (browser opens — click "Create GitHub App")
./create-github-app.sh hh-arc-<group>
# Install it on its repo(s) via the printed URL, e.g.
# https://github.com/apps/hh-arc-<group>/installations/new
# Store creds in AWS Secrets Manager (prod). Auto-resolves installation_id.
aws sso login --profile prod # if the session expired
./store-app-secret.sh hh-arc-<group>
If reusing an existing App, the only action here is to add the new repo to
that App’s installation in the GitHub UI
(https://github.com/apps/<app-slug>/installations/new → select the repo). The
org installation_id is unchanged, so re-running store-app-secret.sh is not
required.
2. Add a setup script for the runner
Each runner set has a small setup_arc_<repo>.sh in
github-runners/hetzner-k3s-arc-runners/. Copy an existing single-repo one
(e.g. setup_arc_hh-relay.sh) and edit the marked values.
cd hungryhub-terraform/github-runners/hetzner-k3s-arc-runners
cp setup_arc_hh-relay.sh setup_arc_<repo>.sh
Edit setup_arc_<repo>.sh:
#!/bin/bash
set -euo pipefail
INSTALLATION_NAME="arc-runner-set-<repo>" # the runs-on label
NAMESPACE="arc-runners-<repo>" # k8s namespace
GITHUB_CONFIG_URL="https://github.com/hungryhub-team/<repo>"
# GitHub App auth — point ARC_APP_NAME at the App that owns this repo (step 0/1)
ARC_APP_NAME="hh-arc-<group>"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=_arc_github_app.sh
source "${SCRIPT_DIR}/_arc_github_app.sh"
trap cleanup_github_app_creds EXIT
load_github_app_creds "${ARC_APP_NAME}"
use_arc_kubeconfig "${SCRIPT_DIR}"
echo "==> Deploying ${INSTALLATION_NAME} (namespace: ${NAMESPACE})"
helm uninstall "${INSTALLATION_NAME}" --namespace "${NAMESPACE}" 2>/dev/null || true
helm install "${INSTALLATION_NAME}" \
--namespace "${NAMESPACE}" \
--create-namespace \
--values custom_spec_cpu2.yml \ # pick a tier (see below)
--set githubConfigUrl="${GITHUB_CONFIG_URL}" \
--set githubConfigSecret.github_app_id="${GH_APP_ID}" \
--set githubConfigSecret.github_app_installation_id="${GH_APP_INSTALL_ID}" \
--set-file githubConfigSecret.github_app_private_key="${GH_APP_PEM_FILE}" \
oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set
echo "==> Done: ${INSTALLATION_NAME}"
Pick a resource tier (--values)
| Spec file | Runner | Use for |
|---|---|---|
custom_spec_cpu1.yml | 1 cpu / 2Gi | lint, static analysis, simple scripts |
custom_spec_cpu2.yml | 2 cpu / 4Gi | unit tests, small Docker builds |
custom_spec_cpu4.yml | 4 cpu / 8Gi | integration tests, standard Docker builds |
custom_spec_cpu8.yml | 8 cpu / 16Gi | full image builds, e2e, heavy compile |
custom_spec_amd64.yml | amd64 node pool | jobs that need x86_64 specifically |
(The cpuN ratio is 2 GiB RAM per runner core; see
Hetzner k3s Self-Hosted Runners §1.)
3. Deploy it
cd hungryhub-terraform/github-runners/hetzner-k3s-arc-runners
aws sso login --profile prod # if session expired
bash setup_arc_<repo>.sh
The script fetches the App creds from AWS, pins KUBECONFIG to the k3s cluster,
and helm installs the scale set. helm uninstall first makes it idempotent.
4. Verify
export KUBECONFIG="$PWD/k3s_kubeconfig.yaml"
# Secret must hold App keys only — NEVER a github_token
kubectl get secret arc-runner-set-<repo>-gha-rs-github-secret -n arc-runners-<repo> \
-o jsonpath='{.data}' | python3 -c 'import json,sys;print(sorted(json.load(sys.stdin).keys()))'
# -> ['github_app_id','github_app_installation_id','github_app_private_key']
# Listener should come up 1/1 Running on image 0.14.2
kubectl get pods -n arc-systems | grep <repo>
# Listener log should show App auth + "Getting next message" (no 403 / 404)
kubectl logs -n arc-systems <listener-pod> --tail=10
Then in the repo’s workflow:
jobs:
build:
runs-on: arc-runner-set-<repo>
steps: ...
Push a workflow run and confirm a runner pod appears in arc-runners-<repo>.
If the listener won’t start
A repo that previously had a runner (old PAT era, or a prior chart version) can
hit the cleanup-protection finalizer deadlock: a stuck Terminating secret,
a stale github_token merged into the secret, or an orphan 0.13.x
AutoscalingListener CR. Use the migration helper instead of the plain setup
script — it clears all three before reinstalling:
bash migrate-to-app.sh setup_arc_<repo>.sh arc-runner-set-<repo> arc-runners-<repo>
Full explanation + manual recipe: Hetzner k3s Self-Hosted Runners §6.
Commit the new setup script
The runner set is defined by setup_arc_<repo>.sh — commit it to
hungryhub-terraform so the deploy is reproducible and reviewable. Never commit
the App private key or kubeconfig (both gitignored).
Related
- How to Access the Hetzner k3s Cluster
- Hetzner k3s Self-Hosted Runners
github-runners/github-app-provisioning/README.md— GitHub App provisioning