Skip to content
FA Ferdous Azad

Run GitHub Actions Runners on Your Infrastructure

Run GitHub Actions runners on your own infrastructure to manage CI costs, with practical setup steps and isolation rules for untrusted pull requests.

#1. Introduction

Self-hosted runners can make CI cheaper when you have steady workloads or spare compute, but they also put your machines and network within reach of workflow code. The safe way to adopt them is to treat each runner as a disposable worker, especially when a job could run code from an untrusted pull request.

The project is GitHub’s open-source Actions Runner: the software that connects a machine to GitHub Actions and executes its jobs. It’s useful for teams that need control over compute, specialized hardware, or access to private infrastructure. The cost advantage depends on how much you run and what it takes to operate the workers; a self-hosted label alone doesn’t make CI cheaper.

#Key features

  • Runs GitHub Actions jobs on machines you manage.
  • Supports Linux, macOS, and Windows runner platforms.
  • Uses labels to route jobs to runners with the right operating system or capabilities.
  • Supports ephemeral runners that register for one job and then deregister.

#Quick start

This example registers a Linux runner for one job. Use a disposable test machine and a workflow triggered only by trusted pushes. Don’t use this setup for public pull requests: the machine remains in your control after the job, and workflow code can access anything the runner’s account or network can reach.

First, create a self-hosted runner in your repository’s Settings → Actions → Runners. Select Linux and your machine’s architecture, then follow GitHub’s instructions to download and extract the matching runner package. Generate the registration token through the settings flow; don’t commit it or paste it into a workflow.

After extracting the package, configure and start the runner:

Bash
# Run inside the extracted runner directory.
./config.sh \
  --url https://github.com/OWNER/REPOSITORY \
  --token "$RUNNER_TOKEN" \
  --labels ci-test \
  --ephemeral

./run.sh

--ephemeral tells GitHub to deregister this runner after it handles one job. It doesn’t shut down or clean the machine. Your infrastructure automation must destroy or reset that machine afterward. For a production setup, make the token available only to the provisioning process and avoid writing it to shell history or logs.

Add a workflow that targets the runner’s labels. This one runs on pushes, not pull requests:

.github/workflows/ci.ymlYAML
name: CI

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: [self-hosted, linux, x64, ci-test]
    steps:
      - uses: actions/checkout@v4
      - run: ./scripts/test.sh

Change main, the runner labels, and the test command to match your repository. A job stays queued if no online runner matches all the requested labels. Labels help with routing; they don’t isolate jobs or make a runner safe for untrusted code.

#The cost trade-off

GitHub-hosted runners are managed for you. With self-hosted runners, you pay for the compute and storage you provision, then take on the work of patching, monitoring, scaling, and cleaning up the machines. Those costs can be lower if you already have suitable infrastructure or enough consistent CI work to keep capacity busy. They can be higher if workers sit idle or require substantial operations effort.

Estimate the full cost, not just the VM bill:

Runner cost = compute + storage + network + idle capacity + operations

The equation is a planning tool, not a promise of savings. Start with your current workload: job duration, concurrency, queue time, and machine requirements. Then compare the cost of running equivalent capacity yourself, including the time spent maintaining it. Specialized hardware or private-network access can justify self-hosting even when it isn’t the cheapest option.

#How it compares

Option Cost and effort Isolation
GitHub-hosted runners Managed; billed under GitHub’s terms Managed worker lifecycle
Persistent self-hosted runner Infrastructure and ongoing maintenance Jobs can leave state behind
Ephemeral self-hosted runner Infrastructure plus provisioning automation Runner deregisters after one job

This is a qualitative comparison. Your actual costs depend on your GitHub plan, workload, and infrastructure. Ephemeral registration improves job separation, but it doesn’t clean up the underlying host by itself.

#Isolating untrusted pull requests

A workflow can run attacker-controlled code through build scripts, package installation, tests, or actions. That code runs with the runner process’s permissions. If a persistent runner can read a cloud credential, reach an internal service, or access files left by an earlier job, a malicious pull request may be able to use that access.

For public repositories, don’t run untrusted pull request code on persistent self-hosted runners. GitHub’s documentation warns that pull request workflows can execute code from contributors; the absence of repository secrets doesn’t protect credentials stored on the runner or services reachable from its network.

For workloads that must test outside contributions, use a disposable environment with:

  • A fresh VM or isolated worker for each job, destroyed afterward.
  • No production credentials, privileged mounts, or access to internal services.
  • Restricted network egress, so job code can’t freely reach internal systems.
  • A low-privilege runner account and minimal permissions for the workflow’s GitHub token.
  • Separate worker pools for trusted branches and untrusted contributions.

Ephemeral runners are a useful part of this design, not the whole design. The infrastructure that creates them must also ensure the host starts clean and is destroyed after the job. Avoid pull_request_target workflows that check out and execute a pull request’s code with elevated access; that pattern can expose secrets or write permissions to attacker-controlled code.

#When to use it (and when not to)

Use self-hosted runners when you need a specific machine type, private-network access, or tighter control over where CI runs, and you can maintain the worker lifecycle. They’re also worth evaluating when your workload is steady enough to make your own capacity economical.

Skip them when you don’t have time to patch and monitor machines, when jobs need broad access to production systems, or when a persistent runner would process untrusted code. GitHub-hosted runners may be the simpler choice for ordinary CI, especially if you don’t have a clear infrastructure requirement.

#License and community

The GitHub Actions Runner source repository is published under the MIT license. GitHub Actions itself is a hosted service; running the open-source runner software on your machine doesn’t give you a self-hosted copy of the entire Actions platform. GitHub’s repository and documentation are the places to check for runner setup guidance and current platform details.

#Key takeaways

  • Self-hosting can reduce compute costs, but only after accounting for idle capacity and operations.
  • Runner labels route jobs; they don’t provide security boundaries.
  • Use ephemeral runners with disposable hosts for untrusted workloads, and remove network access and credentials they don’t need.
  • Keep public pull request code off persistent self-hosted runners.

Was this useful?

Get new posts by email

One short email a week with what I wrote. No spam, unsubscribe in one click.

Drafted with AI assistance from the sources above, then reviewed and published by Ferdous Azad. Spotted an error? Let me know.

Comments

No comments yet. Start the conversation.

Leave a comment

First-time comments are reviewed before they appear. Be kind and on topic.

More in Open Source

View all