Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

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:

AppReposAWS secret
hh-arc-serverhh-server/github-runners/arc/hh-arc-server
hh-arc-frontendhh-pegasus, book-bite/github-runners/arc/hh-arc-frontend
hh-arc-platformhungryhub-terraform, hungryhub-ai, hh-relay/github-runners/arc/hh-arc-platform
hh-arc-vendorhhvendor-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 fileRunnerUse for
custom_spec_cpu1.yml1 cpu / 2Gilint, static analysis, simple scripts
custom_spec_cpu2.yml2 cpu / 4Giunit tests, small Docker builds
custom_spec_cpu4.yml4 cpu / 8Giintegration tests, standard Docker builds
custom_spec_cpu8.yml8 cpu / 16Gifull image builds, e2e, heavy compile
custom_spec_amd64.ymlamd64 node pooljobs 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).