Self-Hosted Runners: Complete Guide to Custom GitHub Actions Execution
📅 Published: July 2026
⏱️ Estimated Reading Time: 20 minutes
🏷️ Tags: GitHub Actions, Self-Hosted Runners, CI/CD, DevOps, Infrastructure
Introduction: What is a Self-Hosted Runner?
A self-hosted runner is a system that you deploy and manage to execute GitHub Actions workflows on your own infrastructure. Instead of using GitHub's managed runners, you run the runner application on your own machines—physical servers, virtual machines, cloud instances, or containers .
Why self-hosted runners matter:
Cost savings: No per-minute charges for GitHub Actions (you pay only for your infrastructure)
Custom hardware: Use specific CPU architectures, GPUs, or large memory configurations
Private network access: Connect to internal databases, APIs, and services
Custom software: Install proprietary tools and specific software versions
Part 1: Self-Hosted vs GitHub-Hosted Runners
When to use GitHub-hosted runners :
Quick setup without infrastructure management
Workflows don't need custom dependencies
Open source or public repositories
When to use self-hosted runners :
Custom dependencies or persistent state required
Private network access needed
High-performance hardware required
Cost optimization for high-volume workflows
Part 2: Setting Up a Self-Hosted Runner
Prerequisites
Any machine (physical, virtual, or container)
Must be able to communicate with GitHub
Enough hardware resources for your workflows
Linux machines require Docker installed for container actions
Administrative access on the machine
GitHub repository or organization admin access
Step 1: Prepare the Machine
sudo apt update sudo apt upgrade -y sudo apt install -y curl tar git
Create a directory for the runner :
mkdir actions-runner && cd actions-runner
Step 2: Download and Extract the Runner
# Replace X.Y.Z with latest version curl -o actions-runner-linux-x64-X.Y.Z.tar.gz -L \ https://github.com/actions/runner/releases/download/vX.Y.Z/actions-runner-linux-x64-X.Y.Z.tar.gz tar xzf ./actions-runner-linux-x64-X.Y.Z.tar.gz
Step 3: Obtain a Runner Token
Go to your repository → Settings → Actions → Runners
Click New self-hosted runner
Copy the configuration command provided (contains your token)
Step 4: Configure the Runner
./config.sh --url https://github.com/yourusername/yourrepository --token YOUR_TOKEN --labels self-hosted,my-runner
During configuration, you'll be prompted to:
Enter runner name (press Enter for default)
Enter runner group (press Enter for default)
Add additional labels (optional)
Step 5: Start the Runner
Interactive mode:
./run.sh
As a service (recommended for production) :
sudo ./svc.sh install sudo ./svc.sh start
Step 6: Verify Online Status
Check in GitHub: Repository → Settings → Actions → Runners
🟢 Online → runner is active
🔴 Offline → runner is not running
Step 7: Test with a Workflow
Create .github/workflows/test-runner.yml:
name: Self-Hosted Runner Test on: [push] jobs: test: runs-on: self-hosted steps: - name: Print message run: echo "Self-hosted runner is working!"
Part 3: Runner Organization and Labels
Scopes
Self-hosted runners can be registered at three levels:
| Scope | Access | Use Case |
|---|---|---|
| Repository | Single repository only | Development/testing |
| Organization | All repositories in org | Shared team resources |
| Enterprise | All orgs in enterprise | Centralized management |
Labels for Targeted Execution
Labels allow workflows to select specific runners:
jobs: build: runs-on: [self-hosted, linux, x64, gpu] steps: - name: Build with GPU run: ./build-with-cuda.sh
Labeling during configuration:
./config.sh --labels self-hosted,linux,production,largeExample with multiple runners :
Runner 1:
linux, ubuntu, mediumRunner 2:
linux, ubuntu, large
Workflow runs-on: [self-hosted, ubuntu] can run on either, while runs-on: [self-hosted, ubuntu, large] only runs on Runner 2.
Part 4: Running Runners at Scale
Deployment Options
Option 1: EC2 Auto Scaling Groups
Simple to deploy and understand
Use spot instances for cost savings
Auto-scaling based on CPU utilization
Option 2: Kubernetes (Recommended)
Use
actions-runner-controllerAdvanced horizontal scaling with
HorizontalRunnerAutoscalerScale to zero when idle
Custom runner images without rebuilding AMIs
version: '3.8' services: github-runner-1: build: . environment: - GITHUB_ORG_TOKEN=${GITHUB_ORG_TOKEN} - GITHUB_URL=${GITHUB_URL} - RUNNER_LABELS=docker,self-hosted,linux mem_limit: 2G cpus: 2.0
Containerized Runners Setup
Environment file (.env) :
GITHUB_ORG_TOKEN=ghp_your_actual_token_here GITHUB_URL=https://github.com/YOUR-ORG RUNNER_LABELS=docker,self-hosted,linux,production MEMORY_LIMIT=2G CPU_LIMIT=2.0
Management commands:
# Start runners docker-compose up -d # Check status docker-compose ps # View logs docker-compose logs -f github-runner-1
Part 5: Security Best Practices
Critical Security Warning
⚠️ Never use self-hosted runners with public repositories. This creates a serious security vulnerability where malicious actors could execute code on your infrastructure through pull requests .
Security Model
When a workflow runs on your self-hosted runner, it can:
Execute arbitrary code as the runner user
Access the Docker socket to run any container
Access the host network
Read/write files in the runner's workspace
Workflows CANNOT (by default):
Access files outside the runner's home directory
Execute as root (unless explicitly granted)
1. Restrict Runner Access
Only run workflows from trusted repositories
Require manual approval for external PRs
Use organization-level runners with strict scoping
Never allow untagged jobs (
run_untagged=false)
2. Use Dedicated Runner Infrastructure
❌ BAD: Run runners on production database servers
✅ GOOD: Isolated VMs or bare metal for runner fleet
3. Network Security
# Outbound: Allow HTTPS to GitHub # Inbound: No ports need to be exposed (runners poll GitHub) sudo ufw default deny incoming sudo ufw allow out 443/tcp sudo ufw enable
Consider:
Network segmentation (separate VLAN for runners)
Egress filtering to block access to internal services
Logging network connections for audit
4. Docker Socket Exposure
❌ AVOID: Mounting Docker socket unnecessarily
✅ BETTER: Use specialized runners for Docker builds
# For runners that need Docker: runs-on: [self-hosted, linux, docker]
5. Secrets Management
✅ Use GitHub secrets for sensitive data
❌ Never hardcode secrets in workflows
✅ Use environment-specific secrets
✅ Rotate secrets regularly
6. Container Image Trust
# ✅ GOOD: Pin exact versions container: image: node:20.11.0 # ❌ BAD: Using latest container: image: node:latest
7. Resource Limits
Configure resource limits to prevent resource exhaustion:
sizes: medium: cpus: 6.0 # CPU quota (6 cores) mem_limit: "16g" # Memory limit pids_limit: 4096 # Max processes
8. Ephemeral Runners
For maximum security, use ephemeral runners that are destroyed after each job. This prevents persistent threats and ensures a clean environment for every run.
Key consideration: Ephemeral runners require more infrastructure overhead but provide better security isolation .
9. Monitoring and Auditing
View runner logs:
sudo journalctl -u gha-* -f
Monitor for:
Failed login attempts
Unusual network connections
Resource usage spikes
Container activity (
docker ps,docker logs)
10. Regular Updates
# Update runner binaries # Update version in config, then re-deploy # Update host OS sudo apt update && sudo apt upgrade -y # Update container images docker pull node:20
Schedule:
Weekly: Review security advisories
Monthly: Update runner binaries, OS patches
Quarterly: Audit runner configurations, review access
Part 6: Real-World Experiences
The Guardian's Migration
Why they moved from GitHub-hosted runners:
Cost: iOS builds required macOS runners with 10x minute multiplier
Timeouts: Builds sometimes ran frustratingly slowly
Unpredictable changes: GitHub runner image updates occasionally caused extreme slowdowns
Results:
⚡ Unit tests ran 50% faster
⚡ Upload builds were 60% faster
💰 Monthly bill dropped by roughly £400
🔧 More control over OS upgrades
Challenges faced:
Non-ephemeral runners meant old job data could stick around
Needed cleanup strategies for DerivedData folders
Required physical hardware maintenance
Concurrency management needed
Learnings:
Bigger is better: easier to manage one powerful machine than a fleet
Always-on power supply and remote access are essential
Plan for proper cleanup routines
Quick Reference
# Setup commands mkdir actions-runner && cd actions-runner curl -o runner.tar.gz -L https://github.com/actions/runner/releases/latest/download/actions-runner-linux-x64.tar.gz tar xzf runner.tar.gz ./config.sh --url https://github.com/ORG/REPO --token YOUR_TOKEN ./run.sh # Service management sudo ./svc.sh install sudo ./svc.sh start sudo ./svc.sh stop sudo ./svc.sh status # View logs sudo journalctl -u actions.runner.* -f
Summary
Self-hosted runners give you complete control over your CI/CD execution environment. They offer significant cost savings for high-volume workflows, enable access to private networks, and allow custom hardware configurations. However, they come with increased maintenance responsibilities and security considerations.
| Aspect | Key Points |
|---|---|
| When to use | Cost optimization, custom hardware, private network access |
| Security | Never use with public repos, restrict access, audit regularly |
| Maintenance | OS updates, runner updates, disk space monitoring |
| Scaling | Use Kubernetes or EC2 ASG for production fleets |
Learn More
Practice self-hosted runner setup with hands-on exercises in our interactive labs:
https://devops.trainwithsky.com/
Comments
Post a Comment