self-host →

Fleet installation guide

Deploy RunsOn Fleet on AWS with Terraform or OpenTofu.

RunsOn Fleet is deployed with the official Terraform module. The module is available on the Terraform Registry and the OpenTofu Registry.

Use Fleet when a platform team wants to publish approved runner fleets such as linux-small or linux-large and have repositories target those runner fleets with stable GitHub runner labels.

Prerequisites

  • AWS credentials with permission to create ECS, IAM, EC2, S3, Secrets Manager, and CloudWatch resources.
  • Terraform or OpenTofu.
  • A VPC and at least one public subnet (or use the VPC module to create one). Private subnets are optional.
  • A RunsOn license key.
  • GitHub credentials: a GitHub App for organization mode, or a classic PAT for enterprise mode (see below).

GitHub credentials

Fleet connects to GitHub with a single credential, depending on which boundary it manages:

  • Organization mode — a GitHub App installed on one organization.
  • Enterprise mode — a classic PAT scoped to the enterprise.

Fleet registers runners into a GitHub runner group but never creates one. Create the runner group yourself (in Terraform or the GitHub UI) and reference it by name from fleets.<name>.runner_group. The examples below show this for both single-organization and enterprise topologies.

Organization mode: create a GitHub App

Open the following URL, replacing <ORG> with the organization that will own the App. The query string pre-fills the App name, permissions, and the organization self-hosted-runners write scope:

https://github.com/organizations/<ORG>/settings/apps/new?name=RunsOn%20Fleet%20%5B<ORG>%5D&url=https%3A%2F%2Fruns-on.com&public=false&webhook_active=false&organization_self_hosted_runners=write&actions=read

Or enter your organization name to generate the link directly:

Enter an organization name to generate the link.

Then:

  1. Generate a private key and save the .pem file.
  2. Install the App on the organization (install it on exactly one).
  3. Pass the App ID and the .pem contents as github_app_id and github_app_private_key in the module.

Enterprise mode: create a classic PAT

Open the following URL, replacing <ENTERPRISE> with your enterprise slug. The query string pre-fills the token description and the manage_runners:enterprise scope:

https://github.com/settings/tokens/new?description=RunsOn%20Fleet%20%5B<ENTERPRISE>%5D&scopes=manage_runners%3Aenterprise

Pass this PAT as github_enterprise_pat in the module.

For GHES, replace https://github.com in either URL with your GHES host root, and pass that host root as github_base_url to the module.

Single organization

This example creates an organization runner group, a small-x64 runner shape, and one linux-small fleet that registers into that runner group.

provider "github" {
# GitHub App auth, or a PAT scoped to manage the org runner group.
}
resource "github_actions_runner_group" "platform" {
name = "platform"
visibility = "all"
}
module "runs_on_fleet" {
source = "runs-on/runs-on/aws//modules/fleet"
version = "v3.1.0"
stack_name = "runs-on-fleet"
license_key = var.license_key
environment = "production"
github_app_id = var.github_app_id
github_app_private_key = var.github_app_private_key
vpc_id = var.vpc_id
public_subnet_ids = var.public_subnet_ids
# Optional: ubuntu24-full-x64 is a built-in image name. Declare it only
# when you want to override the default AMI lookup pattern or owner.
images = {
ubuntu24-full-x64 = {
name = "runs-on-v2.2-ubuntu24-full-x64-*"
owner = "898082745236"
platform = "linux"
arch = "x64"
}
}
runners = {
small-x64 = {
cpu = []
extras = []
family = ["t3a.micro"]
image = "ubuntu24-full-x64"
}
}
fleets = {
linux-small = {
runner = "small-x64"
runner_group = github_actions_runner_group.platform.name
}
}
}

To restrict the runner group to specific repositories, set visibility = "selected" and pass selected_repository_ids on the github_actions_runner_group resource. Org-level runner group access is fully manageable in Terraform.

After terraform apply, target the fleet from a workflow:

jobs:
build:
runs-on: runs-on/fleet=linux-small/env=production
steps:
- uses: actions/checkout@v6
- run: echo "Hello from RunsOn Fleet"

Enterprise

In enterprise mode, Fleet registers enterprise-owned runner scale sets. This example creates an enterprise runner group with the integrations/github provider and connects two runner fleets to it.

The example also enables private networking (private_mode = "only") to show what a more locked-down deployment looks like. Private mode is not required: you can run enterprise mode with public subnets exactly like the single-org example, just by setting github_enterprise_pat / github_enterprise_name and dropping the private-mode and NAT pieces.

terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
github = {
source = "integrations/github"
version = "~> 6.0"
}
}
}
provider "aws" {
region = var.aws_region
}
provider "github" {
token = var.github_enterprise_pat
}
data "aws_availability_zones" "available" {
state = "available"
}
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 6.0"
name = "runs-on-fleet-vpc"
cidr = "10.20.0.0/16"
azs = slice(data.aws_availability_zones.available.names, 0, 2)
private_subnets = ["10.20.128.0/20", "10.20.144.0/20"]
public_subnets = ["10.20.0.0/20", "10.20.16.0/20"]
enable_nat_gateway = true
single_nat_gateway = true # Use false for one NAT Gateway per AZ.
enable_dns_hostnames = true
enable_dns_support = true
}
module "vpc_endpoints" {
source = "terraform-aws-modules/vpc/aws//modules/vpc-endpoints"
version = "~> 6.0"
vpc_id = module.vpc.vpc_id
endpoints = {
s3 = {
service = "s3"
service_type = "Gateway"
route_table_ids = module.vpc.private_route_table_ids
}
}
}
resource "github_enterprise_actions_runner_group" "runs_on" {
name = "runs-on-fleet"
enterprise_slug = var.github_enterprise_name
visibility = "all"
allows_public_repositories = false
}
module "runs_on_fleet" {
source = "runs-on/runs-on/aws//modules/fleet"
version = "v3.1.0"
license_key = var.license_key
environment = "production"
github_enterprise_pat = var.github_enterprise_pat
github_enterprise_name = var.github_enterprise_name
vpc_id = module.vpc.vpc_id
public_subnet_ids = module.vpc.public_subnets
private_subnet_ids = module.vpc.private_subnets
private_mode = "only"
# Optional: ubuntu24-full-x64 is a built-in image name. Declare it only
# when you want to override the default AMI lookup pattern or owner.
images = {
ubuntu24-full-x64 = {
name = "runs-on-v2.2-ubuntu24-full-x64-*"
owner = "898082745236"
platform = "linux"
arch = "x64"
}
}
runners = {
linux-small = {
cpu = []
extras = []
family = ["t3a.micro"]
image = "ubuntu24-full-x64"
}
windows-large = {
cpu = 8
ram = 16
extras = []
family = ["m7i"]
image = "windows25-full-x64"
}
}
fleets = {
linux-small = {
runner = "linux-small"
runner_group = github_enterprise_actions_runner_group.runs_on.name
}
windows-large = {
runner = "windows-large"
runner_group = github_enterprise_actions_runner_group.runs_on.name
timezone = "UTC"
schedule = [
{
name = "default"
hot = 1
stopped = 3
},
]
}
}
}

This creates two runner fleets:

  • runs-on/fleet=linux-small/env=production
  • runs-on/fleet=windows-large/env=production

windows25-full-x64 is also a predefined image name, so it does not need an images entry.

Picking which orgs and repos can use the runner group

visibility = "all" lets every org in the enterprise see the runner group. To restrict to specific orgs, set visibility = "selected" and pass selected_organization_ids.

There is no field for selecting specific repositories on an enterprise runner group, neither in Terraform nor in the GitHub UI. To restrict repos within a permitted org, an org admin has to open the org’s Settings → Actions → Runner groups, click the shared enterprise runner group, and pick repositories from there.

NAT Gateway and private mode

Because this example uses private_mode = "only", runners launch in private subnets and need a route to the public internet for github.com, container registries, and package mirrors. That is what the NAT Gateway provides. If you don’t want a NAT Gateway, switch to public subnets by removing private_mode and private_subnet_ids.

The S3 gateway endpoint keeps S3 cache traffic on the AWS network path without adding hourly endpoint cost.

Next steps