Add github-runner skill: deploy, manage, and troubleshoot self-hosted GitHub Actions runners

- SKILL.md with trigger table, quick reference, deployment spectrum, and pitfalls
- references/ for architecture, deployment (systemd/Docker/ARC/Scale Set Client),
  security, autoscaling, management, custom images, and network
- templates/ for docker-compose.yml and custom-runner.Dockerfile
- AGENTS.md updated with trigger row in alphabetical order
This commit is contained in:
Magnus Hedemark
2026-06-23 00:13:55 -04:00
parent 75e77b28a9
commit e151580319
11 changed files with 903 additions and 0 deletions
+1
View File
@@ -57,6 +57,7 @@ When the user mentions these keywords, load the corresponding skill:
| "debug this", "root cause", "why is this broken", "fix this bug" | [systematic-debugging](systematic-debugging/SKILL.md) |
| "epub", "ebook", "EPUB file", "ebook format", "read epub", "write epub", "create ebook", "extract from epub", "epub to text", "edit epub", "repair epub", "convert epub2", "epub images", "batch epub", "ebook metadata" | [epub](epub/SKILL.md) |
| "gutenberg", "public domain", "download a book", "classic literature", "free ebook", "gutenberg.org", "project gutenberg", "PG", "gutendex" | [gutenberg](gutenberg/SKILL.md) |
| "self-hosted runner", "github actions runner", "CI runner", "set up a runner", "runner registration", "runner won't register", "autoscaling runners", "runner security", "runner group", "ARC", "Actions Runner Controller", "runner scale set", "myoung34/github-runner", "ephemeral runner", "just-in-time runner", "runner container image", "runner custom image", "runner network", "runner troubleshooting", "runner monitoring" | [github-runner](github-runner/SKILL.md) |
| "hugo theme", "hugo cms", "accessible theme", "wcag theme", "theme design", "theme accessibility", "theme UX", "design tokens", "css theme", "theme contrast", "responsive theme", "hugo template", "hugo pipes", "hugo module", "hugo shortcode", "render hook", "tailwindcss hugo", "hugo i18n", "hugo seo", "hugo output format", "hugo site", "hugo static site" | [hugo-theme](hugo-theme/SKILL.md) |
| "weather", "forecast", "temperature", "is it raining", "Tempest" | [tempest-cli](tempest-cli/SKILL.md) |
| "reverse-engineer", "understand this codebase", "PRD from code", "architecture document" | [software-architecture-analysis](software-architecture-analysis/SKILL.md) |
+125
View File
@@ -0,0 +1,125 @@
---
name: github-runner
description: >-
Deploy, manage, and troubleshoot self-hosted GitHub Actions runners. Covers
systemd service, Docker containers, Kubernetes (Actions Runner Controller),
and the Scale Set Client. Use when setting up a CI runner, debugging
registration failures, designing autoscaling, or hardening runner security.
license: MIT
compatibility: Linux, macOS, or Windows target hosts. Docker for containerized runners. Kubernetes for ARC deployments.
metadata:
tags: [github-actions, ci-cd, runners, devops, docker, kubernetes, autoscaling]
source: https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners
---
# Self-Hosted GitHub Actions Runner
Deploy and manage self-hosted GitHub Actions runners — the machines that execute CI/CD workflow jobs. Self-hosted runners give you control over hardware, OS, and toolchain, at the cost of requiring you to maintain the environment.
## When to Use
| Trigger | What to do |
|---------|------------|
| "Set up a self-hosted runner for [repo/org]" | Read [deployment](references/deployment.md) — choose systemd, Docker, or ARC |
| "Runner won't register / keeps failing" | Read [management](references/management.md) — ACCESS_TOKEN vs RUNNER_TOKEN, groups |
| "How to scale runners automatically" | Read [scaling](references/scaling.md) — ARC, Scale Set Client, ephemeral |
| "Secure my self-hosted runners" | Read [security](references/security.md) — public repo risks, ephemeral, JIT, groups |
| "Make a custom runner image" | Read [custom-images](references/custom-images.md) — Dockerfile, ARC container modes |
| "What domains does a runner need to reach?" | Read [network](references/network.md) — firewall rules, TLS, proxy |
| "Labels, groups, or both for routing?" | Read [management](references/management.md) — labels and groups sections |
| "Monitor / troubleshoot runner issues" | Read [management](references/management.md) — monitoring and troubleshooting sections |
## Quick Reference
### Deployment Spectrum
| Approach | Complexity | Autoscaling | Best For |
|----------|------------|-------------|----------|
| systemd service | Low | Manual | Single machine, simple CI |
| Docker container | Medium | Manual replicas | Homelab, small team |
| ARC (Kubernetes) | High | Built-in | Teams with K8s expertise |
| Scale Set Client | High | Custom | Non-K8s platform teams |
### Critical: ACCESS_TOKEN vs RUNNER_TOKEN
This is the most common setup failure. The `myoung34/github-runner` Docker entrypoint **unexports `RUNNER_TOKEN`** at startup — it's only used for **de-registration**. Registration requires `ACCESS_TOKEN` (a GitHub PAT).
| Token | Purpose | Expiry |
|-------|---------|--------|
| `ACCESS_TOKEN` | Registration — generates fresh tokens via GitHub API | Long-lived (PAT) |
| `RUNNER_TOKEN` | **De-registration only** — NOT for initial registration | 60 min |
**PAT scopes:**
- Repo-level: `repo`
- Org-level: `admin:org`
- Enterprise-level: `manage_runners:enterprise`
### Labels Strategy
Default labels: `self-hosted` + OS (`linux`/`windows`/`macOS`) + arch (`x64`/`ARM`/`ARM32`/`ARM64`)
Common convention: `self-hosted,<hostname>,<os>,<arch>,<project>`. In workflows:
```yaml
runs-on: [self-hosted, linux, x64, gpu]
```
All labels must match (AND logic). Use `--no-default-labels` to strip OS/arch auto-labels.
### Key Pitfalls
| Problem | Cause | Fix |
|---------|-------|-----|
| 404 on POST to runner-registration | `RUNNER_TOKEN` used instead of `ACCESS_TOKEN` | Switch to `ACCESS_TOKEN` with PAT |
| "Could not find any self-hosted runner group named 'Default'" | Org uses different group name | Check groups via `gh api`, set `RUNNER_GROUP` |
| "Ephemeral option is enabled" when not wanted | `EPHEMERAL=0` — truthy in bash | Use `EPHEMERAL=false` (string) |
| `docker compose down -v` wipes credentials | Named volumes deleted | With `ACCESS_TOKEN`, auto-recovers |
| Runner can't see host filesystem paths | Runner runs inside Docker container | Write deploy configs inline in workflow |
| Hugo build: "Go not found" | No Go on Ubuntu 20.04 runner | `hugo mod vendor` and commit `_vendor/` |
| GHCR pull "unauthorized" | No Docker registry auth in deploy job | Add `docker/login-action@v4` |
| Runner offline >14 days | Auto-removed by GitHub | Register a new runner |
## Sequential Workflow
### 1. Choose deployment approach
Read [deployment](references/deployment.md) and select systemd, Docker, ARC, or Scale Set Client.
### 2. Figure out what runner scope you need
- **Repo-level**: Runner scoped to a single repo — you need admin access
- **Org-level**: Runner shared across repos in an org — you need org owner access
- **Enterprise-level**: Runner shared across orgs in an enterprise — you need enterprise access
### 3. Register the runner
Read the [architecture](references/architecture.md) reference for the registration flow.
### 4. Route jobs to the runner
Use `runs-on` with labels and optionally groups. Read [management](references/management.md) labels section.
### 5. Monitor and troubleshoot
Read the [management](references/management.md) troubleshooting section when things go wrong.
### 6. Plan for security and scaling
Read [security](references/security.md) and [scaling](references/scaling.md) for production deployments.
## Templates
- [templates/docker-compose.yml](templates/docker-compose.yml) — Docker runner with `myoung34/github-runner`
- [templates/custom-runner.Dockerfile](templates/custom-runner.Dockerfile) — Custom runner image for ARC
## Reference Files
| File | Load when |
|------|-----------|
| [references/architecture.md](references/architecture.md) | You need to understand registration flow, job lifecycle, or runner communication |
| [references/deployment.md](references/deployment.md) | You need to deploy a runner — systemd, Docker, ARC, or Scale Set Client |
| [references/security.md](references/security.md) | You need to harden runners, set up ephemeral/JIT, configure groups |
| [references/scaling.md](references/scaling.md) | You need autoscaling — ARC, Scale Set Client, or webhook-driven |
| [references/management.md](references/management.md) | You need groups, labels, monitoring, troubleshooting, or cleanup |
| [references/custom-images.md](references/custom-images.md) | You need a custom runner Dockerfile, ARC Kubernetes mode |
| [references/network.md](references/network.md) | You need firewall rules, proxy config, or are troubleshooting connectivity |
+76
View File
@@ -0,0 +1,76 @@
# Runner Architecture
## Registration Flow
1. **Token generation**: A GitHub PAT (or short-lived registration token) is used to call `POST /actions/runners/registration-token` via the GitHub API. Tokens expire in ~60 minutes.
2. **Configuration**: `config.sh --url <scope> --token <token>` creates `.credentials` and `.runner` files.
- `--labels`: comma-separated custom labels
- `--runnergroup`: target group (fails if group doesn't exist)
- `--ephemeral`: one-job-only mode
- `--disableupdate`: opt out of auto-updates
- `--no-default-labels`: strip OS/arch auto-labels
3. **Connection**: `run.sh` establishes an HTTPS long-poll connection to `*.actions.githubusercontent.com`:
- Sends "listening for jobs" heartbeat
- Receives job assignments in real-time
- Output: `√ Connected to GitHub` followed by `Listening for Jobs`
## Job Assignment Lifecycle
1. Workflow triggers → GitHub Actions service dispatches jobs matching `runs-on` labels/groups
2. Runner receives "Job Available" message via long-poll
3. If idle and online, runner acknowledges and accepts the job
4. If the runner doesn't pick up the assigned job within 60 seconds, the job is re-queued
5. Runner downloads job details, executes steps sequentially
6. Streams logs and status back to GitHub via HTTPS
7. For ephemeral runners: runner deregisters automatically after job completion
8. For persistent runners: runner returns to Listening state
## Routing Precedence
- GitHub matches `runs-on: [self-hosted, linux, x64, gpu]` — runner must match ALL labels
- Runner groups can be specified alongside labels:
```yaml
runs-on:
group: ubuntu-runners
labels: ubuntu-24.04-16core
```
- If no matching runner is online, the job queues for up to 24 hours
- If a runner doesn't pick up an assigned job within 60 seconds, the job is re-queued
## Service Management
| Platform | Command | Notes |
|----------|---------|-------|
| Linux (systemd) | `sudo ./svc.sh install && sudo ./svc.sh start` | Creates unit at `/etc/systemd/system/actions.runner.*` |
| macOS (launchd) | `./svc.sh install && ./svc.sh start` | Creates plist in user's LaunchAgents |
| Windows | Part of config script | Managed via Services app or PowerShell |
| Docker | Container entrypoint handles lifecycle | Named volume persists credentials |
## Service Commands (Linux/macOS)
```bash
./svc.sh install [username] # Install service (Linux: optional user arg)
sudo ./svc.sh start # Start service
sudo ./svc.sh status # Check service status
sudo ./svc.sh stop # Stop service
sudo ./svc.sh uninstall # Remove service
```
## Key Files (on-disk runner installation)
| File | Purpose |
|------|---------|
| `.runner` | Configuration — scope, URL, runner name |
| `.credentials` | Encrypted auth credentials (persisted across restarts) |
| `.credentials_rsaparams` | RSA key pair for authentication |
| `.service` | Service name (written by svc.sh install) |
| `_diag/` | Log files — `Runner_<timestamp>.log`, `Worker_<timestamp>.log` |
| `_update/` | Self-update binaries and logs |
## Automatic Cleanup
- **Persistent runner** offline > 14 days: automatically removed by GitHub
- **Ephemeral runner** offline > 1 day: automatically removed by GitHub
- **JIT runners**: removed after single job or automatically if never used
+80
View File
@@ -0,0 +1,80 @@
# Custom Runner Images
## GitHub's Official Runner Image
Published at `ghcr.io/actions/actions-runner`. Based on `mcr.microsoft.com/dotnet/runtime-deps:8.0-jammy`.
**Contents:**
- Runner binaries
- Runner container hooks (for Kubernetes mode with ARC)
- Docker CLI (for Docker-in-Docker mode)
Tags accompany each runner release + `latest`.
## Building Custom Runner Images
Requirements:
1. Base image must run the runner application (Linux with standard system dependencies)
2. Runner binary at `/home/runner/`
3. Launch via `/home/runner/run.sh`
4. For ARC Kubernetes mode: container hooks at `/home/runner/k8s`
**Example Dockerfile:**
```dockerfile
FROM mcr.microsoft.com/dotnet/runtime-deps:6.0 as build
ARG RUNNER_VERSION="2.322.0"
ARG RUNNER_ARCH="x64"
ARG RUNNER_CONTAINER_HOOKS_VERSION="0.3.1"
ENV DEBIAN_FRONTEND=noninteractive
ENV RUNNER_MANUALLY_TRAP_SIG=1
ENV ACTIONS_RUNNER_PRINT_LOG_TO_STDOUT=1
RUN apt update -y && apt install curl unzip -y
RUN adduser --disabled-password --gecos "" --uid 1001 runner \
&& groupadd docker --gid 123 \
&& usermod -aG sudo runner \
&& usermod -aG docker runner \
&& echo "%sudo ALL=(ALL:ALL) NOPASSWD:ALL" > /etc/sudoers
WORKDIR /home/runner
RUN curl -f -L -o runner.tar.gz \
https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-${RUNNER_ARCH}-${RUNNER_VERSION}.tar.gz \
&& tar xzf ./runner.tar.gz && rm runner.tar.gz
RUN curl -f -L -o runner-container-hooks.zip \
https://github.com/actions/runner-container-hooks/releases/download/v${RUNNER_CONTAINER_HOOKS_VERSION}/actions-runner-hooks-k8s-${RUNNER_CONTAINER_HOOKS_VERSION}.zip \
&& unzip ./runner-container-hooks.zip -d ./k8s && rm runner-container-hooks.zip
USER runner
```
## Installing Software
Options for adding tools to runner environments:
1. **Pre-installed image**: Build a custom Dockerfile with needed tools baked in
2. **Setup actions**: Use `actions/setup-*` in workflows — recommended for standard tools
3. **Inline install**: Add `apt-get` or `pip install` steps to workflow — adds to job runtime
4. **VM base image**: For VM runners, bake tools into the base image
## ARC Container Modes
| Mode | Description | Use Case |
|------|-------------|----------|
| Docker-in-Docker | Runner pod runs Docker inside; steps use Docker actions | Workflows that need containers |
| Kubernetes | Each step runs as its own K8s pod; no Docker needed | Lighter footprint, cleaner isolation |
| Default | Runner binary runs directly in container | Simple workflows, no containers needed |
## Runner Version Management
- Check latest release: https://github.com/actions/runner/releases
- Subscribe to releases for notifications
- **30-day update window** for `--disableupdate` runners — after 30 days, GitHub stops assigning jobs
- **Critical security updates** block jobs immediately until updated
- For Docker runners: update image tag and recreate containers
- For systemd runners: download new binary, stop service, replace, restart
+138
View File
@@ -0,0 +1,138 @@
# Deployment Approaches
Four primary approaches to deploying self-hosted runners.
## 1. systemd Service (Linux)
Simplest approach. Download the runner binary, configure, install as a service.
```bash
mkdir actions-runner && cd actions-runner
curl -o runner.tar.gz -L \
https://github.com/actions/runner/releases/download/v2.322.0/actions-runner-linux-x64-2.322.0.tar.gz
tar xzf runner.tar.gz
./config.sh --url https://github.com/org/repo --token <token>
sudo ./svc.sh install
sudo ./svc.sh start
```
**Pros:** Minimal dependencies, full control, direct filesystem access
**Cons:** Manual updates, no built-in lifecycle management, state lives on the host
## 2. Docker Container (myoung34/github-runner)
Ubuntu 20.04-based image wrapping the runner binary with Docker-specific lifecycle management.
```yaml
services:
runner:
image: myoung34/github-runner:latest
container_name: runner
restart: unless-stopped
environment:
- RUNNER_NAME=my-runner
- RUNNER_SCOPE=org
- ORG_NAME=myorg
- ACCESS_TOKEN=ghp_...
- RUNNER_GROUP=self-hosted
- LABELS=self-hosted,linux,x64
- DISABLE_AUTO_UPDATE=1
- EPHEMERAL=false
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- runner-data:/runner
networks:
- traefik
volumes:
runner-data:
networks:
traefik:
external: true
```
**Pros:** Container isolation, named volume for credentials, Docker socket for DinD builds, easy multi-replica
**Cons:** Cannot see host filesystem paths, Ubuntu 20.04 base (Python 3.8), needs Docker socket for builds
**Critical environment variables:**
| Variable | Purpose | Notes |
|----------|---------|-------|
| `ACCESS_TOKEN` | GitHub PAT for registration | Use this, NOT `RUNNER_TOKEN` |
| `RUNNER_SCOPE` | `org` or `repo` | Determines registration endpoint |
| `ORG_NAME` | GitHub org | Required for org scope |
| `REPO_URL` | Full repo URL | Required for repo scope |
| `RUNNER_GROUP` | Target group | Fails if group doesn't exist |
| `LABELS` | Comma-separated | Job routing |
| `EPHEMERAL` | `false` or `true` | Use string, not `0` |
| `DISABLE_AUTO_UPDATE` | `1` | Docker handles version management |
| `RUNNER_WORKDIR` | `/runner/work` | Job working directory |
```bash
# Use .env file for the token
echo "ACCESS_TOKEN=ghp_..." > .env
# In docker-compose.yml: ${ACCESS_TOKEN} or inline
```
## 3. Actions Runner Controller (ARC) — Kubernetes
GitHub's reference implementation — a Kubernetes operator that orchestrates runner scale sets.
**Architecture:**
1. Controller manager deploys in specified namespace
2. AutoScalingRunnerSet resource registers runner scale set with GitHub API
3. Runner ScaleSet Listener establishes HTTPS long-poll connection
4. When a job arrives, listener patches EphemeralRunnerSet with desired replica count
5. EphemeralRunner Controller requests JIT tokens and creates runner pods
6. Runner pod executes job, deregisters, pod is deleted
**Quickstart:**
```bash
# Install the controller
helm install arc-controller \
oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller \
--namespace arc-system --create-namespace
# Deploy a runner scale set
helm install arc-runner-set \
oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set \
--namespace arc-runners --create-namespace \
--values values.yaml
```
**Container modes:**
- **Docker-in-Docker (dind)**: Runner pod runs Docker inside — heavier, for workflows needing Docker actions
- **Kubernetes mode**: Steps run as individual pods — lighter, cleaner isolation
- **Default**: Runner binary runs directly in container
**Required CRDs:**
- `AutoScalingRunnerSet`
- `EphemeralRunnerSet`
- `RunnerScaleSetListener`
## 4. GitHub Actions Runner Scale Set Client
Standalone Go module for building custom autoscaling outside Kubernetes. Handles GitHub API interactions while you handle infrastructure provisioning.
**Use case:** Platform teams who need custom autoscaling across VMs, containers, on-prem, or cloud. Supports Windows, Linux, macOS.
**Key properties:**
- Orchestrates GitHub API interactions for scale set registration
- Leaves infrastructure provisioning to you
- Multiple labels for flexible job routing
- Real-time telemetry for job execution
- Extensible — customize for specific requirements
**Note:** This is NOT a replacement for ARC. ARC remains the reference Kubernetes implementation. The Scale Set Client is for non-Kubernetes environments.
**Repository:** `actions/scaleset` on GitHub
## Deployment Comparison
| Approach | Complexity | Autoscaling | Security | Best For |
|----------|------------|-------------|----------|----------|
| systemd | Low | Manual | Low | Single machine, simple CI |
| Docker | Medium | Manual replicas | Medium | Small team, homelab |
| ARC (K8s) | High | Yes (Kubernetes) | High | Teams with K8s expertise |
| Scale Set Client | High | Yes (custom) | High | Platform teams, non-K8s |
+119
View File
@@ -0,0 +1,119 @@
# Management & Operations
## Runner Groups
Groups control which repos can access which runners at the org level.
**Key operations:**
- Create: Settings → Actions → Runner groups → New runner group
- Register into group: `./config.sh --runnergroup <name>`
- Move runner between groups: GitHub UI → Settings → Actions → Runners → click runner → Runner group dropdown
- Default group exists in every org; unnamed runners land there
- Restrict repo access: Selected repositories vs All repositories
- Delete group: all runners must be moved or removed first
**Security:** Groups prevent repos in group A from using runners in group B.
## Labels
Labels control job routing at the runner level.
**Default labels:** `self-hosted`, OS (`linux`/`windows`/`macOS`), arch (`x64`/`ARM`/`ARM32`/`ARM64`)
**Custom labels:**
- Add at registration: `./config.sh --labels gpu,fast-ssd`
- Add/remove after registration: via GitHub UI
- Use in workflows: `runs-on: [self-hosted, linux, x64, gpu]`
- All labels must match (AND logic)
- `--no-default-labels`: strip OS/arch auto-labels
- Combine with groups: `runs-on: { group: ubuntu-runners, labels: ubuntu-24.04-16core }`
## Monitoring
### Status in GitHub UI
- **Idle**: Connected, ready for jobs
- **Active**: Currently executing a job
- **Offline**: Not connected
### Log Files
Located in `_diag/` directory:
- `Runner_<timestamp>.log`: App lifecycle, connection status, updates
- `Worker_<timestamp>.log`: Per-job execution details
### Journalctl (Linux systemd runners)
```bash
# Find service name
cat ~/actions-runner/.service
# Follow logs
sudo journalctl -u actions.runner.<scope>.<name>.service -f
```
### Docker Runners
```bash
docker logs <container-name> --tail 20
```
### gh CLI for Runner Status
```bash
# List runners for a repo
gh api repos/<owner>/<repo>/actions/runners --jq '.runners[] | "\(.name) (\(.status))"'
# List runners for an org
gh api orgs/<org>/actions/runners --jq '.runners[] | "\(.name) (\(.status))"'
# Check runner groups
gh api orgs/<org>/actions/runner-groups --jq '.runner_groups[].name'
```
### Network Connectivity Check
```bash
./config.sh --check --url <url> --pat <pat_with_workflow_scope>
```
Tests each required endpoint (github.com, api.github.com, *.actions.githubusercontent.com, etc.) and outputs PASS/FAIL per endpoint. Logs in `_diag/`.
## Troubleshooting
| Issue | Likely Cause | Fix |
|-------|-------------|-----|
| 404 on registration | Using `RUNNER_TOKEN` instead of `ACCESS_TOKEN` | Switch to `ACCESS_TOKEN` PAT |
| "Not configured" crash | No valid credentials; registration failed | Check logs; verify ACCESS_TOKEN or generate fresh token |
| "Could not find any self-hosted runner group named 'Default'" | Org uses differently-named group | Set `RUNNER_GROUP` to the actual group name |
| Ephemeral mode when not wanted | `EPHEMERAL=0` (truthy in bash) | Use `EPHEMERAL=false` |
| `docker: not found` | Docker not installed | Install Docker or the job doesn't need it |
| `Permission denied` on Docker socket | Runner user not in docker group | Add user to docker group or use root |
| `cd /home/user/path` fails inside Docker container | Runner can't see host paths | Write deploy configs inline; use Docker socket only |
| Runner offline >14 days | Auto-removed by GitHub | Register a new runner |
| GHCR pull "unauthorized" | No Docker registry auth in deploy job | Add `docker/login-action@v4` with `secrets.GITHUB_TOKEN` |
## Removing a Runner
**If you have access to the runner machine:**
```bash
# Run the removal command shown in GitHub UI
./config.sh remove --token <token>
```
**If you don't have access:** Use Force remove in the GitHub UI.
**To re-register without re-downloading:** Delete the `.runner` file in the runner directory. Runner can then be re-configured.
## Runner Software Updates
- **Default**: Self-update enabled — runner auto-updates when a job is assigned or within 1 week
- **Disabled**: `--disableupdate` flag — you manage updates via container image
- **30-day window**: If disabled, runner must be updated within 30 days or GitHub stops assigning jobs
- **Critical security updates**: Immediately block jobs until updated
- **Recommendation for Docker runners**: Set `DISABLE_AUTO_UPDATE=1` and update the container image tag instead
Check latest release: https://github.com/actions/runner/releases
## Common Pitfalls
1. **ACCESS_TOKEN vs RUNNER_TOKEN** — most common failure mode
2. **EPHEMERAL=false** as string, not `0` — bash truthiness trap
3. **Missing RUNNER_GROUP** — fails to register if group doesn't exist
4. **Volume cleanup wipes credentials**`docker compose down -v` removes named volumes; ACCESS_TOKEN auto-recovers
5. **Docker container filesystem** — runner cannot see host paths
6. **Ubuntu 20.04 Python 3.8**`dict | None` syntax fails; use `from __future__ import annotations`
7. **Hugo modules need Go** — vendor modules or add `actions/setup-go@v5`
+111
View File
@@ -0,0 +1,111 @@
# Network & Connectivity
## Communication Model
Self-hosted runners connect to GitHub via **outbound HTTPS (port 443) only** — no inbound ports needed.
They poll for jobs via **HTTPS long-poll connections** to `*.actions.githubusercontent.com`.
**Minimum bandwidth:** 70 kbps upload and download.
## Required Domains
### Essential Operations
```
github.com
api.github.com
*.actions.githubusercontent.com
```
### Downloading Actions
```
codeload.github.com
```
### Uploading/Downloading Artifacts, Logs, Caches, Summaries
```
results-receiver.actions.githubusercontent.com
*.blob.core.windows.net
```
### Runner Version Updates
```
objects.githubusercontent.com
objects-origin.githubusercontent.com
github-releases.githubusercontent.com
github-registry-files.githubusercontent.com
```
### OIDC Token Retrieval
```
*.actions.githubusercontent.com
```
### GitHub Packages (Container Registry, etc.)
```
*.pkg.github.com
pkg-containers.githubusercontent.com
ghcr.io
```
### Git LFS
```
github-cloud.githubusercontent.com
github-cloud.s3.amazonaws.com
```
### Dependabot Update Jobs
```
dependabot-actions.githubapp.com
```
### Release Assets
```
release-assets.githubusercontent.com
```
## TLS Verification
- **Enabled by default** — the runner verifies GitHub's TLS certificate
- **Disable for testing only**: Set `GITHUB_ACTIONS_RUNNER_TLS_NO_VERIFY=1`
- **Better approach**: Install GitHub's certificate in the OS trust store
## Firewall Configuration
For strict egress rules:
- Allow outbound HTTPS (443) to all domains listed above
- Some domains use CNAME records — firewalls may need recursive CNAME resolution
- Note: CNAME records may change; the listed domains are stable
## IP Allow Lists
If your GitHub organization or enterprise uses IP allow lists, you must add your self-hosted runner's IP address to the allow list. Without this, the runner cannot communicate with GitHub APIs.
## Proxy Configuration
Self-hosted runners support standard HTTP proxy environment variables:
- `HTTP_PROXY`
- `HTTPS_PROXY`
- `NO_PROXY`
Set these in the service environment or Docker container environment.
## Connectivity Diagram
```
┌──────────────────┐ Outbound HTTPS (443) ┌──────────────────────┐
│ Self-Hosted │ ────────────────────────────> │ GitHub Actions │
│ Runner │ <──────────────────────────── │ Service │
│ (Docker/VM) │ Long-poll for jobs │ *.actions.github. │
│ │ Upload logs & artifacts │ com / api.github. │
│ │ Download action code │ com / blob.core. │
└──────────────────┘ │ windows.net / CDN │
│ └──────────────────────┘
│ Mounted Docker socket (if using DinD)
┌──────────────────┐
│ Host Docker │
│ Daemon │
│ (builds, runs) │
└──────────────────┘
```
+82
View File
@@ -0,0 +1,82 @@
# Autoscaling
Four approaches to autoscaling self-hosted runners.
## 1. Actions Runner Controller (ARC) — Reference Implementation
ARC is GitHub's recommended Kubernetes-based autoscaling solution.
**How it scales:**
1. Runner ScaleSet Listener holds HTTPS long-poll connection to GitHub Actions Service
2. When a job matches the scale set's labels, the listener receives a "Job Available" message
3. The listener checks if it can scale up (within configured max limits)
4. If yes, it acknowledges and patches the EphemeralRunnerSet to increase replica count
5. EphemeralRunner Controller creates runner pods with JIT tokens
6. Each pod runs one job as ephemeral runner, then is deleted
7. Idle runners are scaled down when no jobs are queued
**Helm chart configuration controls:**
- `minReplicas` / `maxReplicas` — scaling boundaries
- `scaleDownDelaySecondsAfterScaleUp` — cooldown timer
- `scaleUpAdjustment` / `scaleDownAdjustment` — scaling step size
- `scaleDownDelaySeconds` — idle timeout before scale down
## 2. GitHub Actions Runner Scale Set Client
Standalone Go module for custom autoscaling outside Kubernetes.
**Use when:**
- You need VM-based autoscaling (AWS EC2, Azure VMSS, GCP)
- You have on-premise infrastructure
- You need multi-platform support (Windows, Linux, macOS)
- ARC's Kubernetes dependency is not a fit
The client handles GitHub API interactions for scale sets. You write the infrastructure provisioning layer that creates and destroys runner instances.
**Repository:** `actions/scaleset` on GitHub
## 3. Webhook-Driven Autoscaling
Use the `workflow_job` webhook to detect job lifecycle events:
| Event Action | Scaling Action |
|--------------|----------------|
| `workflow_job` with `action: queued` | Scale up — deploy new runner |
| `workflow_job` with `action: completed` | Scale down — remove idle runners |
**Considerations:**
- Webhook delivery is not guaranteed timely — can introduce delays
- For larger volumes, use ARC or Scale Set Client instead
- Requires building and maintaining custom automation
## 4. Ephemeral Runner Pattern (Simple Deployments)
For Docker Compose or script-based setups:
1. Listen for `workflow_job` webhooks at org/repo level
2. When jobs queue, deploy new ephemeral runner containers
3. Each container runs with `--ephemeral` flag
4. After one job, runner deregisters and container exits
5. Cleanup process removes exited containers and prunes credentials
**Not recommended** for persistent runner autoscaling — GitHub cannot guarantee jobs aren't assigned to runners being shut down.
## Scaling Recommendations
| Scale | Approach | Complexity | Efficiency |
|-------|----------|------------|------------|
| 1-5 runners | Static Docker Compose | Low | Good |
| 5-50 runners | ARC (K8s) or Scale Set Client | High | Best |
| 50+ runners | ARC (K8s) | High | Best |
| Mixed platform | Scale Set Client | High | Best |
| PoC / low budget | Webhook + Docker | Medium | Moderate |
## Ephemeral vs Persistent
| Aspect | Persistent | Ephemeral |
|--------|------------|-----------|
| Job isolation | Low — shared environment | High — clean per job |
| Auto-scaling | NOT recommended | Recommended |
| Deregistration | Manual / auto after 14d offline | Auto after 1 job |
| Log retention | On-disk in `_diag/` | Must forward externally |
| Setup complexity | Lower | Higher (need provisioning) |
+112
View File
@@ -0,0 +1,112 @@
# Security Hardening
Self-hosted runners are fundamentally less secure than GitHub-hosted runners because they lack ephemeral, clean-slate VM isolation.
**Core rule:** Self-hosted runners should almost never be used on public repositories. Forks can execute arbitrary code on your runner infrastructure.
## Mitigation Strategies
### 1. Runner Groups (Access Boundaries)
Group runners at the org level and restrict which repos can access them. This limits blast radius if a runner is compromised.
```bash
# Check existing groups
gh api orgs/<org>/actions/runner-groups --jq '.runner_groups[].name'
# Register runner into a specific group
./config.sh --url <url> --token <token> --runnergroup <group-name>
```
**Key points:**
- Default group allows all repos in the org — highest risk configuration
- Create custom groups with restricted repository access
- Public repository access is blocked by default per group (can be overridden)
- Move runners between groups in the GitHub UI
### 2. Ephemeral Runners
Ephemeral runners execute at most one job, then deregister automatically.
```bash
./config.sh --url <url> --token <token> --ephemeral
```
- Each job gets a clean environment (if provisioning creates one)
- Logs must be forwarded externally — they're lost when the runner disappears
- GitHub guarantees only one job per ephemeral runner (cannot guarantee for persistent runners)
- Requires automation to provision clean environments
### 3. Just-in-Time (JIT) Runners
Create ephemeral runner configurations via the REST API — no long-lived registration tokens needed.
```bash
# Generate JIT config
curl -X POST https://api.github.com/orgs/<org>/actions/runners/generate-jitconfig \
-H "Authorization: Bearer <token>" \
-d '{"name":"jit-runner","runner_group_id":1,"labels":["self-hosted","linux","x64"]}'
# Use the config at startup
./run.sh --jitconfig <encoded_jit_config>
```
- Runner runs one job, then is automatically removed by GitHub
- Use automation to ensure a clean environment per JIT run
### 4. Secrets Management
- Use `GITHUB_TOKEN` with minimum required permissions
- Never store structured data (JSON, XML, YAML) as a single secret
- Register generated secrets with `::add-mask::VALUE` so they're redacted from logs
- Rotate secrets periodically
- Use environment-level required reviewers for sensitive secrets
- Consider OpenID Connect (OIDC) for cloud resource auth instead of long-lived secrets
### 5. Script Injection Mitigation
- Prefer actions over inline scripts when handling user-supplied values
- Use intermediate environment variables for safe untrusted input:
```yaml
- name: Check PR title
env:
TITLE: ${{ github.event.pull_request.title }}
run: |
if [[ "$TITLE" =~ ^octocat ]]; then
echo "PR title starts with 'octocat'"
fi
```
- Avoid `pull_request_target` trigger unless absolutely necessary
### 6. Third-Party Action Security
- Pin actions to a full-length commit SHA (immutable)
- Audit action source code before using it
- Pin to tags only when you trust the verified creator
- Use Dependabot to keep actions updated and receive vulnerability alerts
- Use OpenSSF Scorecards to flag risky practices
- Use dependency review to screen new/changed workflow dependencies
### 7. Workflow-Level Hardening
- Prevent Actions from creating or approving PRs (org/repo setting)
- Use CODEOWNERS to require review on `.github/workflows/` changes
- Enable code scanning (CodeQL) with GitHub Actions scanning
- Audit Actions events via security log and audit log
### 8. Runner Machine Hardening
- Keep sensitive data off the runner machine (SSH keys, API tokens, internal network access)
- Use OIDC instead of long-lived cloud credentials
- Consider clean VM/container per job execution for sensitive workflows
- Review the GitHub Advisory Database (`ecosystem:actions`) for vulnerabilities
## Summary
1. **Never** use self-hosted runners for public repositories
2. **Isolate** runners by group — restrict repo access
3. **Prefer ephemeral/JIT** runners for workloads needing isolation
4. **Pin actions** to commit SHAs, not tags
5. **Use ACCESS_TOKEN** with minimum scopes, not RUNNER_TOKEN with admin scopes
@@ -0,0 +1,29 @@
FROM mcr.microsoft.com/dotnet/runtime-deps:6.0 as build
ARG RUNNER_VERSION="2.322.0"
ARG RUNNER_ARCH="x64"
ARG RUNNER_CONTAINER_HOOKS_VERSION="0.3.1"
ENV DEBIAN_FRONTEND=noninteractive
ENV RUNNER_MANUALLY_TRAP_SIG=1
ENV ACTIONS_RUNNER_PRINT_LOG_TO_STDOUT=1
RUN apt update -y && apt install curl unzip -y
RUN adduser --disabled-password --gecos "" --uid 1001 runner \
&& groupadd docker --gid 123 \
&& usermod -aG sudo runner \
&& usermod -aG docker runner \
&& echo "%sudo ALL=(ALL:ALL) NOPASSWD:ALL" > /etc/sudoers
WORKDIR /home/runner
RUN curl -f -L -o runner.tar.gz \
https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-${RUNNER_ARCH}-${RUNNER_VERSION}.tar.gz \
&& tar xzf ./runner.tar.gz && rm runner.tar.gz
RUN curl -f -L -o runner-container-hooks.zip \
https://github.com/actions/runner-container-hooks/releases/download/v${RUNNER_CONTAINER_HOOKS_VERSION}/actions-runner-hooks-k8s-${RUNNER_CONTAINER_HOOKS_VERSION}.zip \
&& unzip ./runner-container-hooks.zip -d ./k8s && rm runner-container-hooks.zip
USER runner
@@ -0,0 +1,30 @@
services:
runner:
image: myoung34/github-runner:latest
container_name: runner
restart: unless-stopped
environment:
# Required - change these for your environment
- RUNNER_NAME=my-runner
- RUNNER_SCOPE=org # or "repo"
- ORG_NAME=myorg # required for org scope
# - REPO_URL=https://github.com/owner/repo # required for repo scope
- ACCESS_TOKEN=***
# Optional
- RUNNER_GROUP=self-hosted # must match an existing group
- LABELS=self-hosted,linux,x64
- RUNNER_WORKDIR=/runner/work
- DISABLE_AUTO_UPDATE=1
- EPHEMERAL=false # use string "false", not "0"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- runner-data:/runner
volumes:
runner-data:
# Uncomment if you need a shared network
# networks:
# default:
# external:
# name: traefik