Self-hosted runners are just small processes you run on your own machines that GitHub Actions uses to execute jobs.
Let’s see one in action. Imagine we have a simple workflow file, .github/workflows/build.yml:
name: Build and Test
on: [push]
jobs:
build:
runs-on: [self-hosted, linux]
steps:
- uses: actions/checkout@v4
- name: Run a one-line script
run: echo "Hello, $USER!"
- name: Run a multi-line script
run: |
echo "This is a multi-line script."
echo "Current directory is: $(pwd)"
When you push code to your repository, GitHub Actions will look for a job that matches runs-on: [self-hosted, linux]. It sends the job details to a runner registered with your organization or repository that has both the self-hosted and linux labels. Your runner process, listening for these jobs, picks it up. It then checks out your code, executes the specified shell commands, and reports the output back to GitHub.
The core problem self-hosted runners solve is executing jobs in environments where GitHub-hosted runners can’t operate. This could be due to:
- Security Requirements: Your code might need to run on machines that are isolated from the public internet, or within a specific network segment.
- Access to Proprietary Resources: Jobs might need to interact with internal databases, on-premises build tools, or licensed software that isn’t available on GitHub-hosted runners.
- Cost Optimization: For very frequent or long-running jobs, using your own hardware can be more cost-effective than paying for GitHub-hosted runner minutes.
- Custom Hardware: You might need specific CPU architectures (like ARM on your own servers) or specialized hardware for testing.
Here’s how you set it up:
-
Register a Runner:
-
Go to your GitHub repository or organization settings.
-
Navigate to
Actions->Runners. -
Click
Add runner. -
Choose
Self-hostedand your operating system (e.g., Linux, Windows, macOS). -
GitHub will provide a shell command. It will look something like this for Linux:
./config.sh --url https://github.com/your-org/your-repo --token YOUR_REGISTRATION_TOKEN --labels self-hosted,linux -
Run this command on the machine you want to use as a runner. This downloads the runner software and registers it with your repository/organization. You’ll need to replace
https://github.com/your-org/your-repowith your actual GitHub repository URL andYOUR_REGISTRATION_TOKENwith the token provided. The--labelsargument is crucial for matching jobs in your workflow file.
-
-
Run the Runner Service:
-
After configuration, you’ll typically start the runner as a service. For Linux, this often involves:
sudo ./run.sh -
This command starts the runner process, which then polls GitHub for jobs matching its labels. It keeps running in the background, ready to accept work.
-
-
Define Workflows:
- In your
.github/workflowsdirectory, create or modify a workflow file (likebuild.ymlabove). - Crucially, use the
runs-onkey to specify the labels your self-hosted runner has. For example,runs-on: [self-hosted, windows]will only target runners labeledself-hostedandwindows. You can use multiple labels for finer-grained targeting.
- In your
The most surprising thing is how little the runner process itself needs. It’s a Go binary that downloads the workflow, executes the steps using standard shell interpreters (like Bash or PowerShell) or Docker, and streams logs back. It doesn’t need to be a full CI/CD agent with complex integrations; it’s a lightweight job executor. The actions/checkout@v4 action, for instance, is just a pre-packaged script that uses git clone on the runner’s local filesystem.
Think of the runner as a worker bee. You tell GitHub (the queen bee) which skills (labels) your worker has. When a task (job) comes up that matches those skills, GitHub assigns it to one of your available workers. The worker then does the actual work on its own machine.
The next step is often managing secrets for your self-hosted runners, as they’ll need to access sensitive information to perform tasks like deploying to private infrastructure.