A GitHub Actions self-hosted runner is a machine that your team deploys and manages to execute GitHub Actions jobs. It can be a physical server, VM, containerized environment, or cloud instance. GitHub provides the runner application; you provide and operate the compute, operating system, network, tooling, security, and lifecycle.
For most teams, GitHub-hosted runners should remain the default. They remove most runner infrastructure work and give jobs clean managed environments. Self-hosted runners become attractive when you need custom hardware, private-network placement, specialized build images, persistent caches, or enough steady CI usage to justify owning the compute.
The key decision is not simply “GitHub Actions minutes vs a VM.” It is managed execution vs infrastructure ownership.
GitHub Actions self-hosted runner vs GitHub-hosted at a glance
| Decision area | GitHub-hosted runner | Self-hosted GitHub Actions runner |
|---|---|---|
| Infrastructure | Managed by GitHub | Managed by your team |
runs-on example | ubuntu-latest | [self-hosted, linux, x64] |
| Job environment | Managed and provisioned by GitHub | Persistent unless you make it ephemeral |
| Scaling | GitHub handles capacity | You design capacity or autoscaling |
| Custom software | Standard images or larger-runner options | Full OS and toolchain control |
| Private network access | Available through supported hosted-runner patterns | Direct placement in your network |
| Hardware choice | GitHub runner catalog | Your VM, server, architecture, or accelerator |
| Security burden | Lower infrastructure burden | You own patching, isolation, cleanup, and incident response |
| Cost model | Usage-based runner billing, subject to plan allowances | Your infrastructure + operations cost |
| Best default | Standard and bursty CI workloads | Specialized, private, or sustained CI workloads |
A self-hosted runner is not automatically faster, cheaper, or safer. It gives you more control and transfers 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 can:
- download repository code;
- prepare the job workspace;
- execute shell commands and actions;
- access secrets and tokens made available to the job;
- reach network services available from the runner;
- build containers and artifacts;
- upload logs and artifacts;
- report the result back to GitHub.
That makes the runner part of the security boundary. A runner is not just disposable CPU. It is a machine that may temporarily hold source code, credentials, deployment tokens, build artifacts, and access to internal systems.
How runs-on chooses a GitHub Actions runner
The runs-on key tells GitHub where a job should execute.
A GitHub-hosted Linux job commonly uses:
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci - run: npm test
A basic self-hosted job can use:
jobs: test: runs-on: self-hosted steps: - uses: actions/checkout@v4 - run: npm ci - run: npm test
For more controlled routing, use labels:
jobs: build: runs-on: [self-hosted, linux, x64] steps: - uses: actions/checkout@v4 - run: ./build.sh
GitHub routes the job only to an online, idle runner that matches all requested labels. If no matching self-hosted runner is available, the job stays queued until a matching runner becomes available or the queue timeout is reached.
Custom labels can separate workloads such as:
self-hosted linux x64 build production-deploy gpu high-memory
Runner groups add another control layer by restricting which repositories or organizations can use a set of runners.
GitHub's Actions Runner Controller uses its own scale-set model and does not use the ordinary
self-hostedlabel in the same way as manually registered runners. Follow the current ARC documentation when targeting ARC runner scale sets.
What does a self-hosted GitHub Actions runner actually give you?
Self-hosting gives you control over the machine that runs the job.
That can include:
- exact Linux or Windows version;
- custom packages and SDKs;
- CPU and RAM size;
- local NVMe storage;
- ARM or x64 architecture;
- Docker and container tooling;
- private package mirrors;
- internal DNS and network routes;
- local dependency caches;
- licensed build software;
- GPUs or specialized hardware;
- custom monitoring and security agents.
This is useful when the build environment itself is part of the requirement.
For example, a team compiling a large application may want a high-CPU runner with a warm dependency cache. Another team may need a deployment runner inside a private network that can reach an internal Kubernetes API or package repository without exposing that service publicly.
GitHub-hosted runners are still the best default for most teams
GitHub-hosted runners remove a large amount of operational work.
They are usually the better option when:
- workflows are bursty or unpredictable;
- standard Linux, Windows, or macOS images are enough;
- public or fork-based contributions are accepted;
- the team does not want to patch runner machines;
- jobs benefit from clean environments;
- multiple operating systems are required;
- there is no strong private-network or hardware requirement;
- CI infrastructure is not something the team wants to own.
Before self-hosting, check whether GitHub's standard or larger hosted runners solve the actual problem. Owning runners only makes sense when the extra control has measurable value.
When self-hosted GitHub Actions runners make sense
A self-hosted runner is a strong fit when one or more of these constraints are real.
Private network access
A runner may need to reach services that are intentionally not public:
- internal package registries;
- private databases used for integration testing;
- staging environments;
- internal APIs;
- deployment targets;
- private Kubernetes clusters;
- infrastructure control planes.
Putting the runner inside the same controlled network can simplify that access, but the network permissions should remain narrow.
Custom hardware or large builds
Self-hosting can make sense when jobs need:
- large amounts of RAM;
- sustained CPU;
- GPUs;
- ARM hardware;
- large local scratch disks;
- specialized drivers;
- tools not available on standard hosted images.
Stable, high CI utilization
A VM that is busy most of the day can have a more predictable cost profile than usage-based runner billing. A VM that sits idle 95% of the time may be the opposite.
Measure actual workflow minutes and concurrency before making the switch.
Persistent caches and local mirrors
A self-hosted runner can keep Docker layers, compiler caches, package mirrors, and build dependencies close to the job.
That can reduce build time, but persistent state must be controlled. A cache that improves speed can also create non-reproducible builds or become a persistence path after compromise.
GitHub Actions runner pricing in 2026
GitHub publishes per-minute rates for GitHub-hosted runners. Current standard rates include:
| GitHub-hosted runner | Current rate |
|---|---|
| Linux 1-core x64 | $0.002/min |
| Linux 2-core x64 | $0.006/min |
| Linux 2-core arm64 | $0.005/min |
| Windows 2-core | $0.010/min |
| macOS 3-core or 4-core | $0.062/min |
Larger and GPU runners use separate rates. GitHub also notes that plan allowances, public-repository rules, larger-runner eligibility, and billing behavior vary by runner type and GitHub plan.
A simple Linux example before any included usage is:
10,000 billable Linux 2-core minutes × $0.006/minute = $60
That does not mean a $60 VM is automatically equivalent. Hosted-runner pricing includes managed provisioning and cleanup. A self-hosted runner cost model must include:
- VM or server compute;
- storage;
- idle capacity;
- operating-system maintenance;
- runner updates;
- monitoring and logs;
- image maintenance;
- autoscaling automation;
- security controls;
- engineering time;
- incident response.
The right comparison is total CI operating cost, not price per minute versus price per VM.
When is a self-hosted runner cheaper?
Self-hosting tends to become economically attractive when utilization is steady and the team already has the operational capability to manage the runner fleet.
A useful break-even model is:
Monthly self-hosted cost = runner infrastructure + storage + idle capacity + operational overhead Monthly GitHub-hosted cost = billable runner minutes + larger-runner usage + applicable storage or related usage
Then add engineering ownership to both sides where relevant.
Self-hosted runners can be attractive for sustained Linux builds, private deployments, cache-heavy workflows, or workloads that otherwise require expensive larger runners. GitHub-hosted runners are often cheaper operationally for bursty jobs because you do not pay for an idle runner fleet or maintain the machines.
Security is the biggest self-hosted runner trade-off
The first question should be:
Who can cause code to execute on this runner?
A workflow can potentially access whatever the runner can access. That may include:
- repository tokens;
- deployment credentials;
- package-registry credentials;
- cloud API keys;
- internal services;
- artifacts and caches;
- files left by earlier jobs;
- Docker sockets;
- the runner host itself.
GitHub warns against exposing self-hosted runners to untrusted public-repository pull requests because malicious workflow code can compromise the runner environment.
Even for private repositories, apply the same reasoning to contractors, reusable workflows, third-party actions, broad repository permissions, and internal contributors.
A self-hosted runner should have the smallest practical blast radius.
Persistent vs ephemeral self-hosted runners
A persistent runner processes many jobs over its lifetime. That is easy to operate but leaves more opportunity for state to persist between jobs.
An ephemeral runner registers for one job and is automatically de-registered after processing that job. Your automation then destroys or resets the machine.
GitHub recommends ephemeral runners for self-hosted autoscaling and does not recommend autoscaling persistent runners.
Ephemeral runners reduce several risks:
- files leaking between jobs;
- old credentials remaining on disk;
- toolchain drift;
- a compromised runner receiving future jobs;
- inconsistent cleanup.
They do not make untrusted code safe by themselves. During the job, the runner still has access to every credential, network route, and resource you expose to it.
For production ephemeral runners, forward runner logs externally because the machine may disappear immediately after a failed job.
Self-hosted runner autoscaling
A serious runner fleet needs more than a script that starts a VM.
Autoscaling must handle:
- queued jobs;
- runner provisioning;
- GitHub registration;
- matching labels or runner groups;
- job execution;
- external logs and monitoring;
- de-registration;
- machine destruction;
- failed or abandoned runners;
- capacity limits and concurrency.
GitHub's reference Kubernetes solution is Actions Runner Controller (ARC). It is appropriate when the team already operates Kubernetes and wants runner scale sets managed inside that environment.
For teams without Kubernetes, VM-based ephemeral runners can be simpler. Do not adopt Kubernetes only because the CI runner needs autoscaling unless the wider platform already justifies Kubernetes.
Runner labels and groups prevent accidental job placement
Do not send every workflow to one generic runner pool.
Separate runners by purpose and trust.
For example:
Group: build-runners Labels: linux, x64, build Network: package mirrors only Secrets: build credentials only
Group: deploy-staging Labels: linux, staging-deploy Network: staging environment only Secrets: staging deployment credentials
Group: deploy-production Labels: linux, production-deploy Network: narrow production deployment path Secrets: short-lived production credentials
This structure limits lateral movement. A normal test workflow should not land on a runner that can reach production simply because both are “Linux runners.”
Private network access without over-trusting the runner
Private network placement is one of the strongest reasons to self-host, but it can also create the biggest blast radius.
Do not give a CI runner unrestricted access to an entire production network.
Prefer:
- dedicated runner subnets;
- firewall rules for exact destinations and ports;
- short-lived credentials;
- separate build and deploy runners;
- environment-specific runner groups;
- no direct database access unless a workflow truly requires it;
- external logs and monitoring;
- rapid replacement after suspicious activity.
If a job only needs to call one deployment endpoint, exposing the whole private network is unnecessary.
Sizing a self-hosted GitHub Actions runner
Size the runner from workflow measurements rather than a generic CI recommendation.
Track:
- peak CPU during builds;
- memory high-water mark;
- working-directory size;
- Docker layer and dependency-cache growth;
- average job duration;
- concurrent jobs;
- network transfer;
- queue time.
A small lint/test runner may need very little. Compilers, Docker image builds, Android builds, browser tests, monorepos, or large dependency graphs can require much more CPU, RAM, and storage.
For predictable isolation, run one job at a time on a VM-sized runner unless you have intentionally designed safe multi-job concurrency.
Performance: measure total workflow time
Self-hosting can improve performance when the runner has:
- faster CPUs than the default hosted shape;
- warm dependency caches;
- local Docker layers;
- a nearby package mirror;
- low-latency access to private services;
- storage sized for the build workload.
But compare the entire workflow:
queue time + provisioning time + checkout + dependency restore + build + tests + artifact upload + deployment
A persistent runner might make one build faster while increasing maintenance and security cost. An ephemeral runner may add provisioning time while producing cleaner isolation.
Optimize the pipeline, not only the benchmark score of the VM.
A practical decision framework
Choose GitHub-hosted runners when:
- jobs are bursty;
- standard images work;
- you need Linux, Windows, and macOS without operating all three;
- public or untrusted pull requests are common;
- the team wants minimal runner maintenance;
- private access can be solved through a supported hosted-runner pattern;
- CI infrastructure is not a competitive advantage.
Choose self-hosted runners when:
- the build needs custom hardware or software;
- private-network placement is a hard requirement;
- jobs are sustained enough to justify owned capacity;
- persistent/local caches materially improve throughput;
- compliance requires a controlled environment;
- runner images can be rebuilt and replaced automatically;
- the team can own patching, logging, security, and incident response.
The strongest reason to self-host is a concrete constraint. “It might be cheaper” is not enough without measuring utilization and operational cost.
How this applies on Raff
A Raff Linux VM can be used as a persistent GitHub Actions self-hosted runner, a runner image builder, or the compute layer for a VM-based ephemeral runner workflow.
A practical architecture is:
GitHub Actions ↓ Runner group / labels ↓ Raff Linux VM runner ↓ Private network or approved public endpoints ↓ Build, test, or deployment target
Use Private Cloud Networks when runners need controlled connectivity to other Raff workloads. Apply Security controls around firewall rules, administrative access, and credentials.
Raff is most relevant when self-hosting solves a real requirement: predictable sustained compute, custom images, private placement, controlled hardware sizing, or integration with infrastructure the team already operates.
For light or irregular CI, GitHub-hosted runners may remain the better operational choice.
GitHub Actions self-hosted runner checklist
Before moving a production workflow to self-hosted runners, verify:
- the self-hosted requirement is documented;
- runner labels and groups separate workload types;
- public/untrusted workflows cannot target privileged runners;
- the OS and runner application have an update process;
- secrets are short-lived and least-privilege where possible;
- private network access is restricted;
- runner logs are preserved externally where needed;
- disk and cache growth are monitored;
- ephemeral runners are used for autoscaling where practical;
- compromised runners can be replaced instead of repaired;
- cost is measured against actual GitHub-hosted usage;
- queue time and job duration are monitored.
Frequently asked questions
What is a GitHub Actions self-hosted runner?
A self-hosted runner is a machine that you deploy and manage to execute GitHub Actions jobs. GitHub supplies the runner application while you manage the compute, OS, software, networking, security, and lifecycle.
How do I use a self-hosted runner in GitHub Actions?
Register the runner at the repository, organization, or enterprise level, then target it with the workflow's runs-on field. A common example is runs-on: [self-hosted, linux, x64].
Are GitHub Actions self-hosted runners free?
GitHub does not charge GitHub Actions runner minutes for jobs executed on self-hosted runners, but you still pay for the machines, storage, networking, maintenance, logging, security, and engineering required to operate them.
Is a self-hosted runner cheaper than GitHub-hosted?
It can be for sustained, well-utilized workloads, especially when you already need custom hardware or private network placement. GitHub-hosted runners are often more economical operationally for bursty workloads because there is no idle runner fleet to manage.
What does runs-on: self-hosted mean?
It tells GitHub Actions to route the job to an eligible self-hosted runner rather than a GitHub-hosted image. Add labels such as linux or x64 when you need more precise routing.
Should self-hosted runners be ephemeral?
For autoscaling, yes where practical. GitHub recommends ephemeral self-hosted runners and does not recommend autoscaling persistent runners. An ephemeral runner processes one job and then de-registers.
Can a self-hosted runner access a private network?
Yes. That is a common reason to self-host. Keep the runner's network access limited to the services the workflow actually needs because workflow code inherits the runner's reachable network surface.
Can I run Docker jobs on a self-hosted runner?
Yes. GitHub's current requirements state that workflows using Docker container actions or service containers need a Linux self-hosted runner with Docker installed.
Sources
- GitHub Docs — Self-hosted runners
- GitHub Docs — Self-hosted runners reference
- GitHub Docs — Choosing the runner for a job
- GitHub Docs — Using labels with self-hosted runners
- GitHub Docs — Actions runner pricing
- Date verified: 2026-08-28
Conclusion
GitHub Actions self-hosted runners are useful when control over compute, network placement, tooling, or sustained capacity matters enough to justify owning runner infrastructure.
For most teams, GitHub-hosted runners remain the safer default because GitHub handles provisioning, capacity, and much of the machine lifecycle. When you self-host, treat the runner as privileged infrastructure: isolate it, route jobs with labels and groups, use narrow credentials, monitor it, and prefer replaceable ephemeral runners for autoscaling.
If a self-hosted runner is justified, a Raff Linux VM can provide the controlled compute layer while private networking and firewall rules limit what the CI workload can reach.
