Self-Hosted Runners

Setup, labels, ephemeral vs persistent, scaling, runner groups, and security.

Module: Self-Hosted Runners

← Back to all modules

Self-Hosted Runners

GitHub-hosted runners are convenient, but sometimes you need your own infrastructure — for specialized tools, internal network access, long-running jobs, or cost control. Self-hosted runners give you full control over the machine running your jobs.

Certification tip: Self-hosted runners are not automatically updated or patched by GitHub. You manage security, capacity, and maintenance. They also don’t have the same spending limits as GitHub-hosted runners.


1. What Is a Self-Hosted Runner?

A self-hosted runner is a machine you own and manage that executes GitHub Actions jobs. You register it to a repository, organization, or enterprise, then reference it with runs-on: self-hosted or a custom label.

Registration levels

LevelScopeWhere to register
RepositoryOne repo onlyRepo Settings → Actions → Runners
OrganizationSelected repos in orgOrg Settings → Actions → Runner groups
EnterpriseAll orgs in enterpriseEnterprise Settings → Actions → Runners

2. Adding a Self-Hosted Runner

Step 1: Add the runner

In your repo, go to Settings → Actions → Runners → New self-hosted runner. Choose your OS (Linux, Windows, macOS).

Step 2: Download and configure

GitHub provides a download command and configuration script:

mkdir actions-runner && cd actions-runner
curl -o actions-runner-linux-x64-2.311.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.311.0/actions-runner-linux-x64-2.311.0.tar.gz
tar xzf ./actions-runner-linux-x64-2.311.0.tar.gz
./config.sh --url https://github.com/OWNER/REPO --token TOKEN

The token is one-time use and expires after registration.

Step 3: Run the runner

./run.sh

The runner now appears in the Actions tab and can pick up jobs.

Step 4: Run as a service (production)

sudo ./svc.sh install
sudo ./svc.sh start

3. runs-on for Self-Hosted Runners

jobs:
  build:
    runs-on: self-hosted
    steps:
      - run: echo "Running on my own machine"

With custom labels

Labels let you target specific runner capabilities:

jobs:
  build-linux:
    runs-on: [self-hosted, linux, x64]
    steps:
      - run: echo "Linux x64 runner"

  build-arm:
    runs-on: [self-hosted, linux, arm64]
    steps:
      - run: echo "ARM64 runner"

  build-windows:
    runs-on: [self-hosted, windows]
    steps:
      - run: echo "Windows runner"

Labels are configured during runner registration or in the runner settings UI.


4. Ephemeral vs Persistent Runners

Ephemeral runners

Ephemeral runners are removed after a single job completes. This is the recommended, secure option.

./config.sh --ephemeral
AspectEphemeral
LifespanOne job
StateClean slate each job
SecurityHigh — no leftover state
Best forMost CI/CD workloads

Persistent runners

Persistent runners stay registered and reuse disk state between jobs.

AspectPersistent
LifespanUntil manually removed
StatePersists between jobs
SecurityLower — potential state leakage
Best forLong-running builds, cached dependencies

Certification tip: Always prefer ephemeral runners for security. Persistent runners can leak credentials, cache, or build artifacts between jobs.


5. Runner Groups

Runner groups let you organize runners and control which repositories can use them.

Organization-level runner groups

# Targeting a runner group (GitHub Enterprise / organization)
runs-on:
  group: production-runners
  labels: [linux, x64]
# Or with self-hosted + group
runs-on: [self-hosted, production-runners]

6. Runner Application Version

Keep the runner application updated. Outdated runners may not support new features or security fixes.

Check version:

./run.sh --version

Update by downloading the latest runner package and re-running config.sh.


7. Security Considerations

Self-hosted runners run code from your repository (and potentially from PRs). Key risks:

RiskMitigation
Untrusted code from fork PRsDon’t run self-hosted runners on fork PRs
Persistent state leakageUse --ephemeral
Network access to internal systemsIsolate runners, limit firewall rules
Runner compromiseRotate runner tokens, monitor logs
No automatic patchingManually patch the OS and runner

Certification tip: Never use self-hosted runners for pull requests from public forks. The code runs with access to your runner’s environment and network.


8. Visual: Runner Lifecycle

flowchart TD
    A[Register runner<br/>config.sh] --> B[Runner appears in GitHub]
    B --> C[Workflow triggered]
    C --> D{Runner available?}
    D -->|Yes| E[Job assigned to runner]
    D -->|No| F[Job queues]
    E --> G[Execute steps]
    G --> H{Runner type?}
    H -->|Ephemeral| I[Runner removed after job]
    H -->|Persistent| J[Runner stays registered]
    I --> K[Clean state next job]
    style A fill:#bbf,stroke:#333
    style I fill:#bfb,stroke:#333
    style J fill:#f9f,stroke:#333

9. Real-World Use Case: Internal Docker Registry

Scenario: Your company has an internal Docker registry that GitHub-hosted runners can’t reach. You need a self-hosted runner in your VPC.

Runner setup

# On an EC2 instance in your VPC
./config.sh --url https://github.com/myorg/myrepo --token TOKEN --ephemeral --labels docker-registry-access

Workflow

name: Build and Push to Internal Registry

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: [self-hosted, docker-registry-access]
    steps:
      - uses: actions/checkout@v4
      - run: |
          docker build -t internal-registry.mycompany.com/app:${{ github.sha }} .
          docker push internal-registry.mycompany.com/app:${{ github.sha }}

The runner has network access to internal-registry.mycompany.com, which GitHub-hosted runners cannot reach.


10. Hands-On Exercise: Self-Hosted Runner

Part 1: Register a runner

  1. Go to your repo → Settings → Actions → Runners → New self-hosted runner
  2. Choose Linux, download the runner package
  3. Run ./config.sh with the provided URL and token
  4. Run ./run.sh
  5. Confirm the runner appears as “Idle” in the runner list

Part 2: Target the runner

Create .github/workflows/self-hosted.yml:

name: Self-Hosted Test

on:
  workflow_dispatch:

jobs:
  info:
    runs-on: self-hosted
    steps:
      - run: |
          echo "Runner OS: ${{ runner.os }}"
          echo "Runner name: ${{ runner.name }}"
          echo "Working directory: ${{ runner.temp }}"
          uname -a

Verification checklist

Exam connection: This covers runs-on: self-hosted, labels, ephemeral vs persistent, runner groups, and the security risk of fork PRs — all common exam topics.


11. Quick Decision Guide

You want to…Use this
Run on your own infrastructureruns-on: self-hosted
Target specific hardware/OSLabels (e.g. self-hosted, linux, arm64)
Clean state every job--ephemeral runner
Reuse cached state between jobsPersistent runner
Restrict runner to specific reposRunner groups
Access internal network/registrySelf-hosted runner in your VPC
Run fork PR code safelyGitHub-hosted runners only — never self-hosted for forks

Next module: Security & OIDC →