A GitHub Actions self-hosted runner is a machine that your team deploys and operates to execute GitHub Actions jobs. GitHub provides the runner software and workflow orchestration; your team owns the compute, operating system, network access, tooling, patching, isolation, logging, and runner lifecycle.
For most teams, GitHub-hosted runners should remain the default. They are easier to operate, each job runs in a fresh GitHub-managed environment, and bursty workloads do not require you to keep idle CI machines online. Self-hosted runners make sense when a real requirement justifies owning the infrastructure: private-network placement, custom hardware or software, controlled images, specialized build tooling, or sustained CI usage where dedicated compute is economically reasonable.
Raff Technologies is relevant only in that second case. A Raff VM can provide the controlled compute layer for a self-hosted runner, but the runner remains self-managed infrastructure. Raff does not remove the security, patching, isolation, or CI-operations responsibilities that come with self-hosting.
GitHub-hosted vs self-hosted runners: quick verdict
| Decision area | GitHub-hosted runner | Self-hosted runner |
|---|---|---|
| Compute lifecycle | GitHub manages it | Your team manages it |
| Job environment | Fresh instance per job | Persistent unless you deliberately make it ephemeral |
| Scaling | GitHub handles capacity | You provision fixed capacity or autoscaling |
| OS/toolchain control | Standard or larger runner options | Full machine-level control |
| Private network placement | Available only through supported hosted patterns | Direct placement inside your chosen network |
| Hardware choice | GitHub runner catalog | Your VM/server configuration |
| Runner patching | GitHub-managed | Your responsibility |
| Security blast radius | Lower infrastructure ownership | Depends on runner isolation and reachable resources |
| Cost model | Usage-based, with plan allowances and runner-specific rates | Infrastructure + idle capacity + operations |
| Best fit | Standard, bursty, public, or low-ops CI | Private, specialized, sustained, or infrastructure-heavy CI |
A self-hosted runner is not automatically faster, cheaper, or safer. It gives you more control by transferring more responsibility to your team.
What is a GitHub Actions runner?
A runner is the system that receives and executes a GitHub Actions job.
During a workflow, the runner may:
- download repository code;
- execute shell commands and actions;
- access the
GITHUB_TOKENand approved secrets; - reach network services available from the machine;
- build containers and artifacts;
- use local caches;
- upload logs and artifacts;
- deploy to staging or production targets.
That makes the runner part of your security boundary. It is not simply disposable CPU. A compromised runner may expose source code, credentials, artifacts, network routes, or deployment access that the workflow can reach.
How runs-on chooses a runner
GitHub Actions uses runs-on to decide where a job executes.
A GitHub-hosted Linux job can use:
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - run: npm ci - run: npm test
A self-hosted job can use:
jobs: test: runs-on: [self-hosted, linux, x64] steps: - uses: actions/checkout@v6 - run: npm ci - run: npm test
Labels let you route jobs to runners with the required characteristics. Runner groups add another control layer by limiting which repositories can use a pool.
For example:
build-runners labels: linux, x64, build staging-deploy labels: linux, staging-deploy production-deploy labels: linux, production-deploy
Do not route every workflow to one generic pool if some runners have materially more privileged network or deployment access than others.
Why GitHub-hosted runners are the safer default for most teams
GitHub-hosted runners are usually the better choice when:
- jobs are bursty or unpredictable;
- standard Linux, Windows, or macOS environments are sufficient;
- the repository accepts public or untrusted contributions;
- the team does not want to maintain runner operating systems;
- clean job environments are valuable;
- multiple operating systems are needed;
- CI infrastructure is not something the team wants to operate;
- the self-hosting requirement is based only on a vague expectation of lower cost.
GitHub currently provisions a fresh hosted runner instance for each job. That clean-instance model removes an entire category of state-persistence problems that a persistent self-hosted machine has to solve itself.
Before self-hosting, confirm that standard or larger GitHub-hosted runners do not already solve the actual constraint.
When a self-hosted GitHub Actions runner makes sense
Self-hosting is strongest when one of these requirements is explicit.
Private network access
A runner may need to reach infrastructure that should not be publicly reachable:
- internal package registries;
- private staging services;
- deployment endpoints;
- private APIs;
- internal artifact repositories;
- private Kubernetes control planes;
- restricted infrastructure tooling.
Placing a runner inside a controlled network can simplify access, but it also increases the damage a compromised workflow could cause. Give the runner only the routes and credentials the job actually needs.
Custom software or machine configuration
Self-hosting can be justified when the job requires:
- a specific OS image;
- uncommon system packages;
- licensed build software;
- custom drivers;
- high-memory configurations;
- sustained CPU;
- ARM or another required architecture;
- large local scratch space;
- Docker tooling or custom build daemons.
Sustained CI usage
Dedicated compute can become economically attractive when runners are busy enough that predictable infrastructure cost is valuable.
That is very different from a VM that remains idle most of the month.
Measure:
- actual GitHub-hosted billable minutes;
- job concurrency;
- runner utilization;
- queue time;
- CPU and memory peaks;
- cache hit rate;
- engineering time spent operating the runner fleet.
Then compare total cost.
GitHub Actions runner cost: compare the whole operating model
GitHub Actions billing is not a simple “minutes are expensive, VM is cheap” comparison.
GitHub-hosted usage depends on repository type, GitHub plan, included allowances, operating system, and runner size. Standard GitHub-hosted runners are currently free for public repositories, while private repositories receive plan-specific allowances before additional usage is billed. Larger runners have their own pricing rules.
At the time this guide was reviewed, GitHub's published baseline rate for a standard Linux 2-core hosted runner was $0.006 per minute. Use GitHub's live pricing page before making a budget decision because these rates and allowances can change.
A self-hosted model instead looks like:
runner compute + storage + idle capacity + OS maintenance + runner application updates + image maintenance + logging/monitoring + autoscaling automation + security controls + incident response + engineering ownership
A GitHub-hosted model looks more like:
billable hosted-runner usage + larger-runner usage where required + applicable artifact/cache storage + workflow optimization effort
The fair comparison is total CI operating cost at the same reliability and security target.
When is a self-hosted runner actually cheaper?
Self-hosting can be cheaper when:
- workloads are sustained rather than bursty;
- the machine is highly utilized;
- you already operate similar infrastructure;
- the build benefits materially from local caches or mirrors;
- a private runner is required anyway;
- the team can automate rebuilds and replacement;
- runner operations do not consume disproportionate engineering time.
GitHub-hosted runners can remain cheaper when:
- workflows are irregular;
- runner utilization would be low;
- the team would otherwise build a new CI platform from scratch;
- several operating systems are required;
- security/isolation work would be significant;
- the workload fits standard hosted runners well.
The strongest reason to self-host is a documented technical or economic constraint, not simply “a VM looks cheaper than CI minutes.”
Security is the largest self-hosted runner trade-off
The first security question is:
Who can cause code to execute on this runner, and what can that runner reach?
GitHub explicitly warns that self-hosted runners do not inherently provide an ephemeral clean VM for each job and can be persistently compromised by untrusted workflow code.
This matters because a runner can expose:
- repository tokens;
- referenced secrets;
- deployment credentials;
- SSH keys;
- package-registry credentials;
- Docker sockets;
- files left by previous jobs;
- internal services reachable from the network;
- cloud or infrastructure APIs.
Public repositories are a special risk
GitHub recommends that self-hosted runners almost never be used for public repositories because an attacker may be able to submit a pull request that causes untrusted code to execute on the runner.
For open-source or fork-heavy workflows, GitHub-hosted runners should normally remain the default unless you have designed a very specific isolation architecture.
Private repositories are not automatically safe
Private repositories reduce exposure, but anyone or anything capable of triggering a privileged workflow still belongs in the threat model.
Review:
- who can open or modify pull requests;
- reusable workflows;
- third-party actions;
- repository permissions;
- runner-group access;
- secrets exposed to the job;
- network routes available from the runner.
A self-hosted runner should have the smallest practical blast radius.
Persistent vs ephemeral self-hosted runners
A persistent runner handles many jobs over its lifetime. It is simple to understand operationally but allows state to survive unless you clean it correctly.
An ephemeral runner is registered for one job and then de-registered. Your automation can destroy or rebuild the machine afterward.
GitHub currently recommends ephemeral runners for autoscaling and does not recommend autoscaling persistent self-hosted runners.
Ephemeral runners help reduce:
- cross-job files and caches leaking unexpectedly;
- stale credentials on disk;
- toolchain drift;
- a compromised runner receiving future jobs;
- inconsistent cleanup.
Ephemeral does not mean trusted. During its one job, the runner can still access every token, secret, route, and service made available to it.
For production ephemeral fleets, preserve runner logs outside the runner because the machine may disappear immediately after a failed or suspicious job.
Runner software updates are an operational requirement
Self-hosting also means keeping the GitHub runner application current.
By default, the runner software can update automatically. If you disable automatic updates so that you can control the runner image yourself, GitHub currently requires the runner to be updated within 30 days of a new runner release. A runner that falls outside that update window may stop receiving queued jobs, and critical security updates can require faster action.
That means a production self-hosted design needs:
- a runner-image build process;
- version tracking;
- safe rollout/rollback;
- monitoring for outdated runners;
- a replacement process that does not depend on manually fixing individual machines.
Self-hosted runner autoscaling
Autoscaling is where self-hosted runners become a platform engineering problem rather than a single VM problem.
A production design must handle:
- queued jobs;
- capacity decisions;
- runner provisioning;
- GitHub registration;
- job routing;
- log forwarding;
- job completion;
- runner de-registration;
- machine destruction;
- failed/abandoned runner cleanup.
GitHub's recommended Kubernetes-based solution is Actions Runner Controller (ARC). ARC is appropriate when the team already has Kubernetes expertise and wants autoscaling runner scale sets in that environment.
If you do not already operate Kubernetes, adding Kubernetes only to solve CI-runner autoscaling may add more complexity than it removes. VM-based ephemeral runners can be a simpler architecture for smaller teams, but the provisioning and cleanup automation remains your responsibility.
Private network access: useful but dangerous when too broad
Private placement is one of the most valuable self-hosting features and one of the easiest ways to create a large blast radius.
Prefer:
- dedicated runner network segments;
- exact destination/port firewall rules;
- separate build and deploy runners;
- environment-specific runner groups;
- short-lived credentials;
- no database access unless the job genuinely requires it;
- external runner logs;
- runner replacement after suspicious activity.
A workflow that only needs to trigger one deployment endpoint should not automatically receive access to an entire production network.
Sizing a self-hosted runner
Do not choose a runner size from a generic CI recommendation.
Measure:
- peak CPU;
- memory high-water mark;
- working-directory size;
- Docker layer/cache growth;
- average job duration;
- concurrent jobs;
- network transfer;
- queue time.
A lint runner and a monorepo Docker build do not need the same machine.
For predictable isolation, a simple model is one active job per VM-sized runner unless you have intentionally designed and validated multi-job concurrency.
Measure workflow performance, not only VM performance
A faster VM does not automatically create a faster CI system.
Measure:
queue time + provisioning time + checkout + dependency restore + build + tests + artifact upload + deployment
A persistent runner can reduce dependency restore time while increasing security and maintenance burden. An ephemeral runner can improve isolation while adding provisioning time.
Optimize total workflow duration and reliability rather than only CPU benchmark scores.
GitHub-hosted vs self-hosted decision framework
Choose GitHub-hosted runners when:
- workflows are bursty;
- standard images work;
- public or untrusted pull requests are common;
- Linux, Windows, and macOS are all needed;
- the team wants minimal runner maintenance;
- CI infrastructure is not a core internal capability;
- a larger hosted runner solves the performance requirement.
Choose self-hosted runners when:
- private-network placement is a hard requirement;
- the workflow needs custom software or machine configuration;
- sustained utilization justifies dedicated compute;
- local caches/mirrors materially improve throughput;
- compliance requires a controlled runner environment;
- runner images can be rebuilt and replaced automatically;
- the team can own patching, logging, isolation, and incident response.
If you cannot name the specific requirement that GitHub-hosted runners fail to meet, stay GitHub-hosted.
Running a GitHub Actions self-hosted runner on Raff
A Raff VM can provide the compute layer for a GitHub Actions self-hosted runner when self-hosting is already justified.
A simple architecture is:
GitHub Actions ↓ Runner group / labels ↓ Raff VM runner ↓ Restricted private or public network path ↓ Build, test, or deployment target
Use Raff VPC when the runner needs private connectivity to other Raff workloads. Keep firewall rules and credentials narrower than the VPC itself.
Raff VMs are self-managed at the guest OS and application level, so your team remains responsible for:
- installing and registering the GitHub runner;
- OS patching and hardening;
- runner software updates;
- build tooling;
- secret handling;
- logs and monitoring;
- cache cleanup;
- runner isolation;
- autoscaling or replacement automation;
- incident response inside the guest environment.
Raff is a good fit when you need predictable dedicated runner compute, controlled sizing, private placement, or a VM-based runner architecture you are prepared to operate. For light or irregular CI, GitHub-hosted runners may still be the better choice.
