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:
- 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.
- 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.
| Requirement | Minimum | Recommended | Why it matters |
|---|---|---|---|
| Subnets | At least one public and one private subnet | Three public and three private subnets in different availability zones | Spread across zones maximizes instance type availability and keeps a single zone event from stalling the queue |
| IPs per subnet | Enough for peak concurrent runners | 250 per subnet | Every running job holds an address, so the subnet is the real concurrency ceiling |
| Internet connectivity | Every subnet must have it | Same | Runners fetch dependencies, register with GitHub, and report status |
| Private subnet routing | NAT gateways | Same | Runners with static IPs use the addresses of the NAT gateways as their external addresses |
| Route tables | Internet-bound traffic through the Internet Gateway | Same | Broken egress routing shows up as registration timeouts rather than as a network error |
| Quotas | vCPU, EBS, and IP headroom in the region | Raise before migration | Quota 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.
| Destination | Used for | Consequence if blocked |
|---|---|---|
api.warpbuild.com | Runner registration and lifecycle | Instances launch and never pick up a job |
api.github.com | GitHub API calls from the runner and from actions | Job setup fails and status never reports back |
github.com | Git operations, including actions/checkout | Checkout fails on the first step |
| Package registries you depend on | npm, PyPI, crates.io, Maven Central, apt or yum mirrors, and your own registry | Dependency installs fail partway through the build |
| WarpBuild S3 storage over HTTPS | Runner telemetry | Jobs 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 lintPermissions 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
| Responsibility | Your team | WarpBuild |
|---|---|---|
| Provisioning the VPC, subnets, and security group | Owns it, through create mode or an existing network in import mode | Authors the CloudFormation template and both flows |
| Route tables, DNS, NAT gateways, and quota headroom | Owns it | Documents the required shape |
| IAM permissions on runner workloads | Owns it, through the instance profile attached per runner | Injects no permissions into runner workloads |
| The cloud bill for EC2, EBS, and data transfer | Pays AWS directly at your rates | Not billed |
| Runner lifecycle, queue watch, and image patching | Escalates issues | Owns it |
| Build data, caches, logs, and artifacts | Held in your account and your region | Control 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.
| Input | Value | Source |
|---|---|---|
| Jobs per month | 3,000 | Your workflow run history |
| Job duration | 6 minutes | Your workflow run history |
| Runner shape | 8 vCPU, 32 GB Linux | Your current label |
| Monthly runner minutes | 18,000 | 3,000 times 6 |
| WarpBuild BYOC fee, Linux | $0.002 per minute | Pricing page |
WarpBuild hosted warp-ubuntu-latest-x64-8x | $0.016 per minute | Pricing page |
| GitHub-hosted 8-core Linux larger runner | $0.022 per minute | GitHub Actions billing reference, checked on 2026-08-13 |
| Managed NAT data processing, US East (N. Virginia) | $0.045 per GB | Amazon VPC pricing, checked on 2026-08-13 |
| Managed NAT hourly charge, US East (N. Virginia) | $0.045 per hour | Amazon VPC pricing, checked on 2026-08-13 |
| EC2 and EBS for the instance types you pick | Your account rates | Amazon EC2 pricing |
The arithmetic
18,000 minutes a month priced three ways:
| Path | Platform fee per month | Compute billed by | Total on the WarpBuild invoice |
|---|---|---|---|
| GitHub-hosted 8-core Linux larger runner | 18,000 times $0.022 equals $396.00 | GitHub, inside the same rate | Not applicable |
WarpBuild-hosted warp-ubuntu-latest-x64-8x | 18,000 times $0.016 equals $288.00 | WarpBuild, inside the same rate | $288.00 |
| WarpBuild BYOC in your VPC | 18,000 times $0.002 equals $36.00 | Your 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.