131% more reported CVEs per day in 2026 than last year.

Runners

How to switch GitHub Actions runner providers: labels, caches, secrets, billing and fallback

To switch, connect the provider (usually through a GitHub App), change runs-on to its label, and run the job. Then check six things the label change does not handle: the runner image, caches, network identity, OIDC trust, fallback and billing. Pilot one repository first, with a switch you can undo in one change.

Updated

This guide is vendor neutral. It explains what moves when you change GitHub Actions runner providers, what breaks most often, and how to back out. We build CRACI, one of the providers; it appears where a documented fact applies, and in its own section at the end. To choose a provider first, see GitHub Actions runner providers compared and GitHub-hosted runner alternatives.

What changes and what stays when you switch

GitHub Actions has two halves. GitHub is the control plane: it stores the repository and the workflow files, fires the triggers, stores secrets, assigns jobs, shows logs and checks, keeps artifacts and issues OIDC tokens. The CI runner is where a job executes: a machine with an image, CPU, disk, a network path to the internet and usually a local cache. GitHub assigns a job to a runner, delivers the job's secrets when it starts, and the runner streams logs back.

A runner provider replaces the second half only.

Item Stays with GitHub Moves to the provider
Repository, workflow files, triggersYesNo
Secrets storeYesSecrets are delivered to the provider's machine at job start
Job assignment, logs, checks UIYesThe runner streams logs back
Artifacts from actions/upload-artifactYesNo
OIDC token issuerYesNo, but the token says the runner is self-hosted
Machine, image, CPU, memory, diskNoYes
Outbound network path and IP addressesNoYes
Cache storageDepends on the providerDepends on the provider
Per-minute billing for the moved jobsNoYes

A third-party runner registers with GitHub as a self-hosted runner. That is why GitHub's rules for self-hosted runners apply to it: how labels route jobs, how runner groups limit access, how the OIDC token describes it and how GitHub bills it.

The runs-on change and labels

GitHub looks for a runner that matches the job's runs-on labels and groups. If no matching runner is online, the job stays queued until one comes online; if an assigned runner does not pick the job up within 60 seconds, it is re-queued; and a job that stays queued for more than 24 hours fails (GitHub's self-hosted runner reference). A typo in a label, or a provider app not installed on the repository, shows up as a job that never starts.

Providers encode the machine in one of two ways:

  • In the label. Ubicloud uses labels such as ubicloud-standard-2, Blacksmith blacksmith-2vcpu-ubuntu-2404, and Avrea avrea-ubuntu-latest-2-vcpu, with arm in the label for ARM machines. Changing size means changing the label.
  • In a setup step. CRACI uses one label, runs-on: craci, and the cracicorp/setup@v1 step selects the size (1 to 32 compute units of 1 vCPU and 3 GB, default 2), the image, the network policy or an organization runner template by its slug. If CRACI cannot parse an input, it runs the default environment and writes a warning to the job log; an unknown image or template is rejected.

Make the switch one variable

Put the label in a repository or organization variable instead of hardcoding it. Switching forward or back is then a settings change, not a pull request across every repository.

jobs:
  build:
    # Falls back to GitHub-hosted when the variable is not set
    runs-on: ${{ vars.LINUX_RUNNER || 'ubuntu-latest' }}
    steps:
      - uses: actions/checkout@v7
      - run: make build

  deploy:
    needs: build
    # Stays on GitHub-hosted until you decide to move it
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy.sh

The choice is per job, so build and test jobs can move while a deploy job stays. Variables can be set at organization level and overridden per repository, so one setting can move a group of repositories and one repository can opt out.

Reusable workflows

If the same pipeline is copied across many repositories, move it into a reusable workflow that reads the runner from an input or a variable. Changing runs-on there changes every repository that calls it. Rulesets can require a workflow to run on an organization's repositories, which keeps the shared version in use.

Runner images: compatibility and pinning

GitHub's hosted image carries a large preinstalled toolset and a hosted tool cache. Providers offer either an image compatible with GitHub's or a slimmer one. Jobs often depend on things they never declare:

  • preinstalled language versions and command line tools;
  • passwordless sudo, and systemd for services started with systemctl;
  • a running Docker daemon, with Buildx and Compose;
  • the hosted tool cache that setup-* actions read before they download;
  • free disk space for large builds.

Decision rule: start on the provider's GitHub-compatible image. Move a job to a slim image only after it is green, and add explicit setup-* steps for everything it used implicitly.

Pinning: ubuntu-latest moves to a new release on GitHub's schedule, and ubuntu-24.04 pins the operating system release, not the package versions on it. Pin what matters inside the job: setup actions with explicit versions, and container images by digest. Ask each provider what it lets you pin. The runner application itself has to keep updating to talk to GitHub.

On CRACI, the images are CRACI Slim on x86-64 and ARM64 (Ubuntu 24.04, without systemd) and a full GitHub Actions compatible Ubuntu 24.04 image, selected as ubuntu-24.04 or ubuntu-latest, which follows GitHub's Ubuntu 24.04 runner image and includes the hosted tool cache. Every image has Docker Engine, Buildx and Compose v2.

Caches: what carries over and what starts cold

The first runs on a new provider are often slower, and the usual reason is a cold cache, not a slow machine. How actions/cache works decides what you see (actions/cache README):

  • An entry is found by its key and a version, which is a hash of the cached paths and the compression tool.
  • Entries are scoped by branch; the default branch's entries are available to other branches.
  • By default, a repository can hold up to 10 GB of caches before the oldest entries are evicted. Repository administrators and organization or enterprise owners can raise the limit; usage beyond 10 GB is billed to the account (GitHub dependency caching).
  • A cache saved on Windows is not restored on another operating system, or the other way round, unless enableCrossOsArchive is set; a different compression tool also gives a different version.

Providers serve the cache in one of two ways:

Model First runs Trade-off
The runner uses GitHub's cache service, as GitHub-hosted runners do Existing entries restore Cache traffic crosses the internet between the provider and GitHub
The provider serves the cache API from storage next to its runners Usually empty until the first runs fill it, unless the provider imports or falls through to GitHub's entries Faster restores; entries written there are usually not visible to GitHub-hosted runners

The second point matters for mixed fleets and for rollback: if you switch a job back, it may start cold on GitHub too. Ask which model the provider uses and where the entries live.

Telling a cold cache from a slow runner. Take a Node build that caches a large node_modules directory. On the first run on a new provider, actions/cache reports a miss, the install step downloads everything and the post step saves the cache. From the next run on the same branch, the step reports a hit and the install step shrinks. Compare runs only after the cache is warm: read the cache-hit output, and time the restore step and the install step separately from the build itself. See how the GitHub Actions cache works for keys, scopes and cache poisoning.

On CRACI, actions/cache works as it does today, and the build record marks per package whether it was restored from a CI cache instead of fetched.

Docker layer caching

Mechanism Where layers live What to know
BuildKit type=gha The GitHub Actions cache Same scoping and size rules as above; it needs a builder that supports it
BuildKit type=registry A separate image in your registry Works on any runner and survives a provider switch; you pay registry storage and transfer
Provider-local layer cache or persistent disk Next to the provider's runners Usually the fastest; stays behind if you leave the provider

A registry cache is the portable option (Docker cache backends):

- uses: docker/build-push-action@v7
  with:
    push: true
    tags: ghcr.io/acme/app:latest
    cache-from: type=registry,ref=ghcr.io/acme/app:buildcache
    cache-to: type=registry,ref=ghcr.io/acme/app:buildcache,mode=max

Two jobs that start at the same time restore the same cache and both build the layers it is missing. That happens on any CI, so do not read it as a provider problem.

CRACI offers container layer caching with a local layer cache, a remote layer cache and a bind mount cache.

Secrets, OIDC and cloud credentials

GitHub stores your secrets and delivers them to whichever runner runs the job. Moving a job to a provider means trusting that provider with the secrets of that job. Ask:

  • Does each job get its own virtual machine, or a container on a shared host?
  • Is the machine destroyed after the job, and is anything kept?
  • Which logs does the provider keep? GitHub masks registered secrets in job logs, and says automatic redaction is not guaranteed for transformed values; ask about the provider's own system logs too.

OIDC keeps working, with one catch. GitHub issues the OIDC token, not the runner, so permissions: id-token: write works on any runner. But the token's runner_environment claim is github-hosted or self-hosted (GitHub's OIDC reference), and on a third-party runner it is self-hosted. A cloud trust policy that requires github-hosted rejects the job. For example, a Google Cloud workload identity pool with this attribute condition:

# Before: only GitHub-hosted runners in the acme organization
assertion.repository_owner == 'acme' && assertion.runner_environment == 'github-hosted'

# After: drop the runner check, and narrow by repository and branch instead
assertion.repository == 'acme/app' && assertion.ref == 'refs/heads/main'

Removing the runner check widens who can get credentials, so tighten another condition at the same time. Before the switch, search every cloud trust policy and workload identity configuration for runner_environment.

Pushing images: GITHUB_TOKEN with packages: write pushes to GitHub Container Registry from any runner, and registry logins that use stored secrets work unchanged.

On CRACI, the GitHub App reads your code and your Actions runs and manages the runners it registers for your organization. Each job runs in its own isolated virtual machine, and CRACI does not store your source code.

Network identity: IP allowlists and egress

The new runner leaves from new IP addresses. List everything that filters by source address:

  • database and artifact repository allowlists;
  • SaaS APIs with IP restrictions;
  • partner SFTP servers and internal services behind a firewall.

The options are the provider's static or stable egress IP addresses, or a proxy you control. This is also the moment to decide what jobs may reach at all: an egress policy on the runner stops a compromised dependency from sending secrets to a host the build never needed. See GitHub Actions egress policy.

CRACI offers a stable egress IP range and an egress policy, default deny or allow, that is validated before the job starts and fails closed. Without a network input, the default template allows other outbound connections, so egress control is something you configure.

Mixed fleets: move Linux, keep macOS and iOS where they are

The runner is chosen per job, so one workflow can run its Linux jobs on a provider and its macOS jobs on GitHub-hosted runners. Artifacts uploaded with actions/upload-artifact are stored by GitHub, so a job on one runner can hand files to a job on another.

jobs:
  backend:
    runs-on: ${{ vars.LINUX_RUNNER || 'ubuntu-latest' }}
    steps:
      - uses: actions/checkout@v7
      - run: ./gradlew test

  android:
    runs-on: ${{ vars.LINUX_RUNNER || 'ubuntu-latest' }}
    steps:
      - uses: actions/checkout@v7
      - run: ./gradlew assembleRelease
      - uses: actions/upload-artifact@v7
        with:
          name: android
          path: app/build/outputs/apk/release/

  ios:
    # Signing and the iOS build stay on GitHub-hosted macOS
    runs-on: macos-latest
    needs: backend
    steps:
      - uses: actions/checkout@v7
      - run: xcodebuild -scheme App -configuration Release archive

The jobs left on GitHub still draw on your plan's included usage and are billed by GitHub beyond it. CRACI runs Linux on x86-64 and ARM64 today, with Windows and macOS on the roadmap, so a mixed fleet with CRACI means CRACI for Linux and GitHub-hosted runners for the rest.

Fallback when a provider is down

GitHub has no built-in fallback between runner labels. If no runner for the label is online, the job waits, and after 24 hours it fails. Plan for it:

  • The variable switch above: set it back to ubuntu-latest and new runs go to GitHub-hosted runners.
  • A manual override for urgent runs: a workflow_dispatch input, read as runs-on: ${{ inputs.runner || vars.LINUX_RUNNER || 'ubuntu-latest' }}.
  • timeout-minutes on jobs, so a job that hangs does not hold a deployment for hours.
  • Queue time monitoring, so you notice when jobs wait longer than usual.

Ask each provider whether it offers an automatic fallback and what triggers it. Also plan for the other direction: GitHub queues and assigns every job, so a GitHub Actions outage stops jobs on every runner provider too.

Billing: does GitHub still charge?

GitHub does not charge per minute for self-hosted runners, and third-party runners register as self-hosted, so the minutes you move stop appearing on the GitHub bill (GitHub Actions billing). GitHub still bills:

  • seats on your plan;
  • artifact and cache storage it holds beyond the included allowance;
  • jobs left on GitHub-hosted runners, beyond your plan's included usage, which is not refunded.

During the pilot you pay two invoices. Providers count build minutes differently: GitHub rounds each job up to the whole minute, and some providers meter per second. To size your usage before and after, use the GitHub Actions cost calculator. CRACI charges only for build minutes: $0.002 per vCPU-minute ($0.004 for a default 2 vCPU runner), metered per second, with no monthly fee.

Security isolation: what to ask any provider

Question Why it matters
One virtual machine per job, or containers on a shared host? A shared host lets a compromised job reach its neighbours
Is the machine destroyed after the job? A persistent machine carries files, credentials and processes into the next job
Where is the hardware, and who operates it? Data location, jurisdiction and your customers' contracts
What does the provider store, and for how long? Logs, caches and network data may hold sensitive material
Which certifications does it hold? Your vendor review will ask
Can you control egress? Stops secrets leaving through a malicious package
What can its GitHub App read and change? The app's permissions apply to every repository you install it on

GitHub says self-hosted runners should almost never be used for public repositories, because anyone can open a pull request and compromise the environment, and that just-in-time runners run at most one job before they are removed (GitHub's secure use reference). A fresh virtual machine per job answers most of that concern.

CRACI's answers: each job runs in its own isolated virtual machine on European bare-metal infrastructure. CRACI Corporation Oy's information security management system, for the development and operation of the CRACI platform, is certified to ISO/IEC 27001 by Into Certification, with details in the Trust Center. CRACI stores build metadata, package data, network traces, SBOMs, provenance, caches and audit records, and never your source code. See what CRACI can reach and security.

Test-drive one repository first

  1. Pick a repository with a representative, frequently run Linux workflow, not the release pipeline.
  2. Record a baseline on GitHub-hosted runners: median duration and queue time over the last 30 runs, and billed minutes.
  3. Install the provider's GitHub App on that repository only.
  4. Switch one job through the repository variable, on a branch.
  5. Run it at least ten times, and discard the first three as cache warm-up.
  6. Compare duration, queue time, cache hit rate, failures and cost; check logs, OIDC and outbound calls.
  7. Roll back by changing the variable, and confirm the job returns to GitHub-hosted runners.
Metric GitHub-hosted baseline Provider Pass threshold
Median job durationYour target, for example 30% shorter
Median queue timeNo worse than the baseline
Cache hit rate after warm-upSame as the baseline
Failed runs not caused by the codeNone
Cost per runBelow the baseline
OIDC, allowlists and outbound callsAll work

Measure speed on your own workflow. Every provider publishes its own figures, and the only number that decides your case is the one from your pipeline.

How CRACI fits

CRACI is a GitHub Actions runner. It replaces the runner, not GitHub Actions: a job moves with runs-on: craci, sizes and images are set with cracicorp/setup@v1 or an organization runner template, and runs still appear in GitHub. CRACI runs builds about twice as fast as GitHub-hosted runners, thanks to faster hardware, shorter queue times and caching.

The reason to choose it over other providers is what each job leaves behind: an SBOM of what the build fetched, with a completeness state, recorded under an egress policy that fails closed. docs.craci.com also offers a prompt for coding agents that proposes a per-job migration plan for your workflows. See CI/CD integration.

When CRACI is not the answer

  • You need macOS or Windows runners: they are on CRACI's roadmap.
  • You run GitHub Enterprise Server, which CRACI does not support.
  • Your CI is GitLab CI or Jenkins: both are on the roadmap.
  • Runners must live in your own cloud account: customer-hosted runners are on the roadmap. RunsOn and WarpBuild's bring-your-own-cloud option do that today; see CRACI vs RunsOn and CRACI vs WarpBuild.
  • Your main goal is CI observability and debugging; see CRACI vs Avrea. For running your own runners, see managed vs self-hosted runners.

Switching runner providers: frequently asked questions

What do I need to change to use a different GitHub Actions runner provider?

Install the provider's GitHub App, then change the job's runs-on label to the provider's label. Workflows, triggers, secrets and the GitHub UI stay the same. Before you move important jobs, check the runner image, caches, outbound IP addresses, cloud trust policies that use OIDC, fallback and billing.

Do my GitHub Actions caches carry over to a new runner provider?

It depends on where the provider serves the cache from. If its runners use GitHub's cache service, existing entries restore. If it serves the cache API from storage next to its runners, the cache usually starts empty and fills on the first runs. Ask the provider which model it uses, and whether entries written on its runners are visible to GitHub-hosted runners.

Does Docker layer caching work on third-party runners?

Yes, with any of three mechanisms: BuildKit's type=gha backend, which uses the Actions cache; a registry cache with type=registry, which works on any runner; or a layer cache or persistent disk that the provider keeps next to its runners.

Can I keep macOS and iOS builds on GitHub while Linux jobs move?

Yes. The runner is chosen per job, so Linux jobs can run on a provider while macOS jobs stay on GitHub-hosted runners in the same workflow. Connect them with needs: and pass files with actions/upload-artifact and actions/download-artifact, which GitHub stores.

What happens to a job if the third-party runner is down? Is there a fallback?

GitHub has no built-in fallback between runner labels. A job whose label no online runner matches stays queued and fails after 24 hours. Keep the label in a repository or organization variable so switching back is one change, set timeout-minutes, and ask each provider whether it offers its own fallback.

Do secrets and OIDC still work on third-party runners?

Yes. GitHub stores the secrets and delivers them to the runner at job start, and GitHub issues the OIDC token, so id-token: write works on any runner. The catch: the token's runner_environment claim is self-hosted on a third-party runner, so a cloud trust policy that requires github-hosted rejects the job.

Does GitHub still bill me for minutes if I use another runner provider?

Not for the jobs you move. Third-party runners register with GitHub as self-hosted runners, and GitHub lists self-hosted runners as free of per-minute charges. GitHub still bills seats, storage it holds beyond the included allowance, and jobs left on GitHub-hosted runners. The provider bills its own minutes.

How do I pin the runner image so it does not change under me?

ubuntu-latest moves on GitHub's schedule, and ubuntu-24.04 pins the operating system release but not the package versions on it. Pin what matters inside the job: setup actions with explicit versions, and container images by digest. Ask each provider what it lets you pin; the runner application itself has to update to keep working with GitHub.

How are jobs isolated from each other on a third-party runner?

Providers differ. Ask whether each job gets its own virtual machine or a container on a shared host, whether the machine is destroyed after the job, and what the provider keeps afterwards. A fresh virtual machine per job is the strongest of the common models, because nothing from one job is left for the next.

How do I test a runner provider on one repository first?

Install the provider's GitHub App on one repository, switch one frequently run Linux job through a repository variable on a branch, run it at least ten times, and compare duration, queue time, cache hits, failures and cost against a GitHub-hosted baseline. Roll back by changing the variable.

Move one workflow with us

Book a demo and we will move one of your workflows to CRACI with you, then compare duration, cost and the SBOM it produced.

Book a demo