GitHub Actions Tutorial — Self-Hosted Runners Exercises
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:
- Download the Linux x64 runner application (version 2.319.1)
- Configure it for organization
my-orgwith tokenABC123 - Add custom labels
docker,gpu, andproduction - 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:
build— runs on any self-hosted Linux x64 runnergpu-test— runs only on self-hosted Linux runners with agpulabeldeploy— runs only on self-hosted runners with bothproductionanddockerlabels
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:
- Install the ARC controller in the
arc-systemsnamespace - Deploy a runner scale set named
my-runnersin thearc-runnersnamespace - Configure it for org
my-orgwith 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:
- Connects to repository
my-org/my-app - Has labels
ephemeralandlinux - Runs unattended (no interactive prompts)
- 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:
- The runner finishes executing the job
- It automatically de-registers from GitHub
- The runner process exits
- An external orchestrator (systemd, Docker, Kubernetes, ASG) should restart a new runner instance
- 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:
-
Self-hosted runner on public repo — Anyone can fork and run code on your production machine. Fix: Use GitHub-hosted runners for public repos.
-
runs-on: production— Production runners should not process PR builds. Fix: Use separate runner groups; restrict production runners to specific repos. -
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... -
Unknown action —
some-unknown-org/deploy-actionis not verified. Fix: Audit the action’s source code before using it. -
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"
- 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 →