Running GitHub Actions Runners in Your Own VPC

Run GitHub Actions runners in your own VPC as ephemeral EC2 instances. The network shape AWS needs, the outbound allowlist, the IAM split, and the cost.

Last verified:

You can run GitHub Actions runners inside your own VPC by granting a control plane permission to launch ephemeral EC2 instances in subnets you own, which is what WarpBuild BYOC does through a CloudFormation stack deployed in one region of your AWS account. This guide covers what pushes teams into a VPC, the network shape AWS asks for, the outbound destinations a locked-down security group has to allow, and who pays for what once the fleet is running.

Diagnosis

Three requirements drive runners into a customer VPC. Each one carries a trade that lands on the team that asks for it, so name the requirement before you build the network.

Private service access

A build step needs something that has no public address: an internal package registry, a database with a private endpoint, a staging service behind a private DNS zone, or an artifact store reachable only from inside the network. Runners outside the VPC cannot see any of it, and the workarounds all degrade. Publishing the service, punching a hole for a rotating IP range, or proxying through a bastion each add a surface that a security review will ask about later.

Placing runners inside the VPC removes the question, and the detail of what a runner can then reach is covered in can GitHub Actions runners reach my private network. The trade is that routing becomes yours. Subnet placement, route tables, DNS resolution, and the security group are now your team's to own and to debug when a job cannot resolve an internal hostname.

Egress control

The second driver is a security policy that says outbound traffic from build machines is allowlisted, and often that a third party sees a stable source IP address. Both are network properties, so both need the runner inside a network you control.

The trade here has a price attached. Stable outbound addresses mean runners in private subnets behind NAT gateways, and every gigabyte that crosses that path is metered. In US East (N. Virginia), AWS charges $0.045 per hour per gateway plus $0.045 per GB of data processed in both directions (Amazon VPC pricing, checked on 2026-08-13). Runners in public subnets skip that meter entirely. Enable static addressing only for the workflows that genuinely require an allowlisted source, which is the same guidance the BYOC documentation gives and the subject of the guide to static IP addresses for GitHub Actions runners.

Audit boundaries

The third driver is where the bytes live. When runners execute in your account, build data, caches, logs, and artifacts stay inside your account and your region, and the evidence you hand an auditor comes from your own CloudTrail and your own tags rather than from a vendor questionnaire. Isolation improves too, because a dedicated VPC or a dedicated AWS account for GitHub Actions limits the blast radius if a runner is ever compromised.

The trade is operational ownership. EC2 quotas, subnet capacity, EBS limits, and the cloud bill all become yours.

Fix

WarpBuild BYOC splits the system so the network stays yours and the operations do not. The control plane runs on WarpBuild and holds the GitHub App connection, the queue watch, and the reconciliation loop. The data plane is a CloudFormation stack in your account: VPC, subnets, security group, S3 bucket, and the EC2 instances themselves. BYOC runs on AWS, GCP, and Azure, and the AWS path is the one that also offers import mode.

Two ways to get the network in place:

  1. Create mode. The CloudFormation template provisions the VPC, subnets, and security group for you. Use this when GitHub Actions gets a dedicated network and you want the documented shape without assembling it yourself.
  2. Import mode. You select an existing VPC, subnets, and security group when creating the stack. Use this when the network already exists under a platform team's Terraform, or when a policy forbids a template from creating network resources. Import mode is available on AWS.

Each job runs on a distinct, freshly provisioned EC2 instance that is terminated when the job finishes, so there is no shared state between jobs and no long-lived machine to patch. Moving a workflow across is a one-line change to runs-on.

Three properties are immutable once the stack exists: the stack name, the S3 bucket, and the region. Decide those before you apply, and give the stack a name that still reads clearly in six months.

On AWS BYOC the covered platforms are Linux and Windows, so a fleet that also builds for Apple platforms keeps those jobs on WarpBuild-hosted macOS runners.

Configuration

The network shape AWS asks for

This is the documented prerequisite list from the AWS BYOC configuration guide. Check each row against your existing VPC before you start the stack, because the two that bite later are IP capacity and subnet spread.

RequirementMinimumRecommendedWhy it matters
SubnetsAt least one public and one private subnetThree public and three private subnets in different availability zonesSpread across zones maximizes instance type availability and keeps a single zone event from stalling the queue
IPs per subnetEnough for peak concurrent runners250 per subnetEvery running job holds an address, so the subnet is the real concurrency ceiling
Internet connectivityEvery subnet must have itSameRunners fetch dependencies, register with GitHub, and report status
Private subnet routingNAT gatewaysSameRunners with static IPs use the addresses of the NAT gateways as their external addresses
Route tablesInternet-bound traffic through the Internet GatewaySameBroken egress routing shows up as registration timeouts rather than as a network error
QuotasvCPU, EBS, and IP headroom in the regionRaise before migrationQuota denials surface as queued jobs with no obvious cause

Generally available Linux and Windows runners do not have plan-level concurrency caps, which means the ceiling on a BYOC fleet is set by your subnet addresses and your EC2 quotas rather than by a plan limit. That is the reason the 250 IP recommendation is worth honoring on day one.

The stack also wires private access paths for S3 and ECR so cache reads and image pulls stay on the AWS network rather than crossing the public internet.

The outbound allowlist

Runners need outbound access and no inbound path. If your security policy replaces the default allow-all egress rule with an allowlist, these are the destinations to open.

DestinationUsed forConsequence if blocked
api.warpbuild.comRunner registration and lifecycleInstances launch and never pick up a job
api.github.comGitHub API calls from the runner and from actionsJob setup fails and status never reports back
github.comGit operations, including actions/checkoutCheckout fails on the first step
Package registries you depend onnpm, PyPI, crates.io, Maven Central, apt or yum mirrors, and your own registryDependency installs fail partway through the build
WarpBuild S3 storage over HTTPSRunner telemetryJobs still run, and runner-side diagnostics go missing

Cache traffic is routed to the stack S3 bucket over the private AWS path, so it does not need an internet allowlist entry.

On the inbound side, be precise about what the default gives you. The stack security group allows ingress from VPC subnet CIDRs, so blocking inbound traffic entirely is a hardening step you apply rather than a default you inherit. The security hardening guide has the shape: remove every inbound rule, keep outbound to the destinations above, and use Network ACLs to stop runners from talking to each other, since runner instances have no reason to communicate.

Pointing workflows at the fleet

BYOC runners register with the warp-custom- prefix followed by the runner name you chose in the dashboard. The label is the only line that changes when a job moves off hosted capacity.

name: integration
on:
  pull_request:

jobs:
  test:
    runs-on: warp-custom-ci-use1-8x
    steps:
      - uses: actions/checkout@v4

      - name: Resolve the internal registry
        run: |
          getent hosts artifacts.internal.example.com
          npm config set registry https://artifacts.internal.example.com/npm/

      - uses: actions/setup-node@v4
        with:
          node-version: 22

      - run: npm ci
      - run: npm run test:integration

  package:
    needs: test
    runs-on: warp-custom-ci-use1-8x
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/amazon-ecr-login@v2
        id: ecr
      - run: |
          docker build -t "${{ steps.ecr.outputs.registry }}/api:${{ github.sha }}" .
          docker push "${{ steps.ecr.outputs.registry }}/api:${{ github.sha }}"

The ECR login step carries no AWS credentials because the instance profile attached to the runner supplies them. Keep a hosted label available for jobs that have no private dependency, so a stack change never blocks every pipeline at once:

  lint:
    runs-on: warp-ubuntu-latest-x64-4x
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run lint

Permissions on the runner itself

Permissions available to a runner workload are exactly the ones you attach. Instance profiles are configured at the runner level, so a deploy runner can hold ECR push rights while a test runner holds none. Require IMDSv2 on every runner to protect instance credentials against SSRF in build steps, and scope each policy to the specific repository or bucket the job touches.

Terraform support exists for BYOC on AWS, so runner sets and images can live in the same module as the network they run in.

Who owns what

ResponsibilityYour teamWarpBuild
Provisioning the VPC, subnets, and security groupOwns it, through create mode or an existing network in import modeAuthors the CloudFormation template and both flows
Route tables, DNS, NAT gateways, and quota headroomOwns itDocuments the required shape
IAM permissions on runner workloadsOwns it, through the instance profile attached per runnerInjects no permissions into runner workloads
The cloud bill for EC2, EBS, and data transferPays AWS directly at your ratesNot billed
Runner lifecycle, queue watch, and image patchingEscalates issuesOwns it
Build data, caches, logs, and artifactsHeld in your account and your regionControl plane metadata only

If regional placement rather than account placement is the actual requirement, WarpBuild-hosted runners cover it. US and EU are the regions WarpBuild names publicly.

Cost or Time Model

Assumptions

Substitute your own numbers. WarpBuild rates come from the pricing page, and the AWS and GitHub figures carry their own links and dates.

InputValueSource
Jobs per month3,000Your workflow run history
Job duration6 minutesYour workflow run history
Runner shape8 vCPU, 32 GB LinuxYour current label
Monthly runner minutes18,0003,000 times 6
WarpBuild BYOC fee, Linux$0.002 per minutePricing page
WarpBuild hosted warp-ubuntu-latest-x64-8x$0.016 per minutePricing page
GitHub-hosted 8-core Linux larger runner$0.022 per minuteGitHub Actions billing reference, checked on 2026-08-13
Managed NAT data processing, US East (N. Virginia)$0.045 per GBAmazon VPC pricing, checked on 2026-08-13
Managed NAT hourly charge, US East (N. Virginia)$0.045 per hourAmazon VPC pricing, checked on 2026-08-13
EC2 and EBS for the instance types you pickYour account ratesAmazon EC2 pricing

The arithmetic

18,000 minutes a month priced three ways:

PathPlatform fee per monthCompute billed byTotal on the WarpBuild invoice
GitHub-hosted 8-core Linux larger runner18,000 times $0.022 equals $396.00GitHub, inside the same rateNot applicable
WarpBuild-hosted warp-ubuntu-latest-x64-8x18,000 times $0.016 equals $288.00WarpBuild, inside the same rate$288.00
WarpBuild BYOC in your VPC18,000 times $0.002 equals $36.00Your AWS account, at your rates$36.00

The BYOC row is incomplete by design, because the compute line is yours. The full monthly figure is 18,000 * ($0.002 + your per-minute EC2 and EBS rate). To fill it in, take the hourly on-demand price of the instance types in your runner configuration, divide by 60, add the EBS cost for a 150 GB volume amortized across the hours it exists, and multiply. Spot capacity and reserved commitments both change that number, and both are levers a hosted runner cannot give you.

Then price the network decision on top:

  • Public subnets, no static addressing: no gateway charge and no per-GB processing charge on the runner path.
  • Private subnets with NAT gateways: a fleet that moves 400 GB a month pays 400 times $0.045 equals $18.00 in data processing, plus 730 hours times $0.045 equals $32.85 per gateway. Three gateways across three availability zones make that $98.55 in fixed charges before a single byte moves.

That is the whole trade in one line item. Static outbound addresses are worth about $100 a month per region in fixed charges plus per-GB processing, which is cheap for the workflows that need an allowlisted source and wasteful for the ones that do not. Split the fleet: one runner configuration in private subnets for the jobs that call an allowlisted third party, and one in public subnets for everything else.

Getting to a number for your own fleet

Pull the last 30 days of workflow runs, group by label, and multiply minutes by the rates above. Then add the AWS side for the BYOC row using your own instance pricing.

That is enough to stand up a stack, move one workflow, and compare a month of real data before committing the network design. If the decision is still open, hosted runners compared with BYOC runners sets the two models side by side.

Cloud list prices change. Re-check the AWS and GitHub links above before quoting these totals internally.

FAQ

Do runners in my VPC need inbound network access?

No. The runner agent opens the connection outward, so nothing has to reach the instance from outside. Note that the default stack security group allows ingress from VPC subnet CIDRs, so removing every inbound rule is a hardening step you apply rather than a default you inherit.

What outbound destinations does a locked-down security group have to allow?

api.warpbuild.com for runner registration and lifecycle, api.github.com and github.com for API and git operations, and the package registries your builds pull from. Runner telemetry also needs outbound HTTPS to WarpBuild S3 storage.

Can I use an existing VPC instead of letting the stack create one?

Yes. Import mode lets you select an existing VPC, subnets, and security group when you create the stack, and it is available on AWS. Create mode provisions the network from the CloudFormation template instead.

Who controls the AWS permissions my build steps get?

You do. Permissions come from the instance profile you attach at the runner level, so each runner can carry a different scope. WarpBuild does not inject additional permissions into runner workloads.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.