GitHub Actions Self-Hosted Runners

A self-hosted runner is a machine you own and operate that GitHub Actions sends jobs to. You install the runner software on your machine, register it with GitHub, and it becomes available as a build target. Self-hosted runners give you full control over hardware, software, networking, and cost — at the expense of managing the infrastructure yourself.

When to Use a Self-Hosted Runner

Use self-hosted when:
  ✓ Jobs need access to your private network or internal databases
  ✓ Builds require specific hardware (GPU, high RAM, custom chips)
  ✓ Your organization bans data from leaving its own servers
  ✓ You have very long-running jobs where GitHub-hosted costs are high
  ✓ You need custom software pre-installed and persistent between runs

Stick with GitHub-hosted when:
  ✓ Standard build tasks (compile, test, deploy to cloud)
  ✓ You do not want to manage server infrastructure
  ✓ Ephemeral, clean environments are important for security

How Self-Hosted Runners Work

Your Machine (Runner)                   GitHub
──────────────────────                  ──────────────────
Runner software polls GitHub ────────►  Job queue
                             ◄────────  Sends job payload
Runner executes steps
Runner sends logs ───────────────────►  Actions tab (live logs)
Runner reports result ───────────────►  Workflow status

The runner software polls GitHub over HTTPS. Your machine initiates the connection — GitHub never needs to reach into your network. This means your runner works even behind a corporate firewall, as long as outbound HTTPS is allowed.

Installing a Self-Hosted Runner

Follow these steps to add a runner to a repository:

  1. Go to your repository on GitHub
  2. Click SettingsActionsRunners
  3. Click New self-hosted runner
  4. Select your runner's operating system and architecture
  5. Run the download and configuration commands shown on the page

The configuration script asks for a token (generated by GitHub) and the runner URL. It registers the runner with your repository automatically.

Example setup commands (Linux):
  mkdir actions-runner && cd actions-runner
  curl -o actions-runner-linux-x64.tar.gz -L \
    https://github.com/actions/runner/releases/download/v2.x.x/actions-runner-linux-x64-2.x.x.tar.gz
  tar xzf ./actions-runner-linux-x64.tar.gz
  ./config.sh --url https://github.com/my-org/my-repo --token YOUR_TOKEN
  ./run.sh

Running the Runner as a Service

Running ./run.sh keeps the runner active only while the terminal is open. Install it as a background service so it starts automatically and survives reboots:

Linux (systemd):
  sudo ./svc.sh install
  sudo ./svc.sh start

macOS (launchd):
  sudo ./svc.sh install
  sudo ./svc.sh start

Windows (Windows Service):
  .\svc.sh install
  .\svc.sh start

Using a Self-Hosted Runner in a Workflow

Reference a self-hosted runner with the self-hosted label in runs-on:

jobs:
  build:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4
      - run: npm install
      - run: npm test

Use labels to target specific runners. Labels identify operating system, capabilities, or location:

jobs:
  gpu-training:
    runs-on: [self-hosted, linux, gpu]
    steps:
      - run: python train.py --epochs 100

The job runs only on a runner that has all three labels: self-hosted, linux, and gpu. GitHub picks any matching available runner automatically.

Runner Labels

Each runner gets default labels based on its operating system and architecture. You can also add custom labels during configuration or through the GitHub UI:

Default labels added automatically:
  self-hosted
  linux / windows / macOS
  x64 / arm64 / arm

Custom labels you can add:
  gpu
  production
  high-memory
  on-premises
  region-us-east

Runner Groups

Organizations can group runners and control which repositories can access each group. This is useful for separating production runners from development runners:

Runner Groups (Organization level):
  ├── Group: production-runners
  │   ├── runner-prod-1
  │   └── runner-prod-2
  │   Accessible by: repo "main-app" only
  │
  └── Group: dev-runners
      ├── runner-dev-1
      └── runner-dev-2
      Accessible by: all repositories

Ephemeral Self-Hosted Runners

Standard self-hosted runners persist between jobs. If one job installs a package or writes a file, the next job can read it — which can introduce hard-to-debug state pollution and security risks.

Ephemeral runners solve this by starting fresh for each job and shutting down after. Create them with the --ephemeral flag:

./config.sh --url https://github.com/my-org/my-repo \
            --token YOUR_TOKEN \
            --ephemeral

Teams typically pair ephemeral runners with automation that spins up a new runner VM before each job and destroys it after. Tools like Actions Runner Controller (ARC) on Kubernetes handle this automatically.

Actions Runner Controller (ARC)

ARC is an open-source Kubernetes operator maintained by GitHub. It watches your workflow queue and automatically scales the number of runner pods up or down based on demand:

No jobs queued  → 0 runner pods running (costs nothing)
3 jobs queued   → 3 runner pods start automatically
Jobs finish     → pods shut down
Spike to 20 jobs → 20 pods start (within configured limits)

This gives you the security and customization of self-hosted runners with the scalability and ephemeral behavior of cloud runners.

Security Considerations for Self-Hosted Runners

  • Never use self-hosted runners on public repositories — anyone can fork and trigger malicious jobs
  • Use ephemeral runners to prevent job-to-job data leakage
  • Run the runner as a non-root user with minimal OS permissions
  • Isolate runners from production systems using network segmentation
  • Keep the runner software up to date — GitHub releases regular security patches
  • Use runner groups to restrict which repositories can access sensitive runners

Monitoring and Maintaining Runners

Monitor your self-hosted runners from the GitHub UI under Settings → Actions → Runners. The dashboard shows each runner's status:

Status indicators:
  Idle     → Connected, waiting for jobs
  Active   → Currently running a job
  Offline  → Not connected to GitHub

Set up automated alerts when runners go offline. A runner that stops responding silently causes all jobs targeting it to queue indefinitely. Most teams configure a heartbeat check using a scheduled workflow that targets the runner and alerts on failure.

Leave a Comment

Your email address will not be published. Required fields are marked *