GitHub Actions Tutorial — Self-Hosted Runners Exercises

3 minute read

Practice Exercises — Self-Hosted Runners

Test your understanding of runner setup, labels, groups, autoscaling, ephemeral runners, and security with these hands-on exercises.


Exercise 1 — Runner Setup Commands

Task: Write the commands to:

  1. Download the Linux x64 runner application (version 2.319.1)
  2. Configure it for organization my-org with token ABC123
  3. Add custom labels docker, gpu, and production
  4. Install and start it as a systemd service
Show Solution
# Create directory
mkdir actions-runner && cd actions-runner

# Download
curl -o actions-runner-linux-x64-2.319.1.tar.gz -L \
  https://github.com/actions/runner/releases/download/v2.319.1/actions-runner-linux-x64-2.319.1.tar.gz

# Extract
tar xzf ./actions-runner-linux-x64-2.319.1.tar.gz

# Configure with custom labels
./config.sh --url https://github.com/my-org \
            --token ABC123 \
            --labels docker,gpu,production

# Install and start as service
sudo ./svc.sh install
sudo ./svc.sh start

# Verify
sudo ./svc.sh status

Exercise 2 — Label-Based Job Routing

Task: Write a workflow with three jobs:

  1. build — runs on any self-hosted Linux x64 runner
  2. gpu-test — runs only on self-hosted Linux runners with a gpu label
  3. deploy — runs only on self-hosted runners with both production and docker labels
Show Solution

name: Label Routing

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: [self-hosted, linux, X64]
    steps:
      - uses: actions/checkout@v4
      - run: make build

  gpu-test:
    needs: build
    runs-on: [self-hosted, linux, gpu]
    steps:
      - uses: actions/checkout@v4
      - run: python test_model.py

  deploy:
    needs: gpu-test
    runs-on: [self-hosted, production, docker]
    steps:
      - uses: actions/checkout@v4
      - run: docker compose up -d

Each runs-on: list requires ALL labels to match. The runner must have every listed label.


Exercise 3 — ARC Installation

Task: Write the Helm commands to:

  1. Install the ARC controller in the arc-systems namespace
  2. Deploy a runner scale set named my-runners in the arc-runners namespace
  3. Configure it for org my-org with minimum 1 runner and maximum 15 runners

Then write a workflow job that uses these runners.

Show Solution
# Install ARC controller
helm install arc \
  --namespace arc-systems \
  --create-namespace \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller

# Deploy runner scale set
helm install my-runners \
  --namespace arc-runners \
  --create-namespace \
  oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set \
  --set githubConfigUrl="https://github.com/my-org" \
  --set githubConfigSecret.github_token="ghp_xxxxx" \
  --set minRunners=1 \
  --set maxRunners=15

Workflow:


jobs:
  build:
    runs-on: my-runners          # matches the Helm release name
    steps:
      - uses: actions/checkout@v4
      - run: echo "Running on ARC!"


Exercise 4 — Ephemeral Runner Configuration

Task: Write the configuration command for an ephemeral runner that:

  1. Connects to repository my-org/my-app
  2. Has labels ephemeral and linux
  3. Runs unattended (no interactive prompts)
  4. Is ephemeral (processes one job then exits)

Then explain what happens after the runner completes a job.

Show Solution
./config.sh --url https://github.com/my-org/my-app \
            --token YOUR_TOKEN \
            --labels ephemeral,linux \
            --unattended \
            --ephemeral

# Start the runner
./run.sh

What happens after a job completes:

  1. The runner finishes executing the job
  2. It automatically de-registers from GitHub
  3. The runner process exits
  4. An external orchestrator (systemd, Docker, Kubernetes, ASG) should restart a new runner instance
  5. The new instance registers fresh — no state from the previous job

Without an orchestrator, the runner simply stops and no new jobs are processed.


Exercise 5 — Security Audit

Task: Review this workflow configuration and identify all security issues:


# Public repository workflow
jobs:
  build:
    runs-on: [self-hosted, production]
    steps:
      - uses: actions/checkout@v4
      - uses: some-unknown-org/deploy-action@main
      - run: echo ${{ github.event.pull_request.body }}
      - run: curl http://internal-api.company.com/deploy

Show Solution

Security issues found:

  1. Self-hosted runner on public repo — Anyone can fork and run code on your production machine. Fix: Use GitHub-hosted runners for public repos.

  2. runs-on: production — Production runners should not process PR builds. Fix: Use separate runner groups; restrict production runners to specific repos.

  3. Unpinned action: @main — Branch references can be changed by the action author. Fix: Pin to a full SHA: uses: some-unknown-org/deploy-action@abc123...

  4. Unknown action — some-unknown-org/deploy-action is not verified. Fix: Audit the action’s source code before using it.

  5. Script injection: ${{ github.event.pull_request.body }} — PR body is user-controlled input. If it contains backticks or shell metacharacters, it can execute arbitrary commands. Fix: Use an environment variable:


env:
  PR_BODY: ${{ github.event.pull_request.body }}
steps:
  - run: echo "$PR_BODY"

  1. Internal API access — The runner can reach internal-api.company.com. Fix: Isolate runners in a DMZ with restricted network access.

← Back to: Chapter 12: Self-Hosted Runners

Next Chapter: Chapter 13: Deployments →

Updated: