Multi-tenant runner platform for Kubernetes
Self-hosted GitHub Actions with zero idle compute¶
No idle workers. Disrupted jobs auto-retry. Enforceable quotas.
GitHub Actions Gateway (GAG) is an Actions Runner Controller (ARC) alternative for shared, multi-tenant clusters.
Get started Watch the demo Why GAG? View on GitHub
Drop-in for your existing setup: jobs target the same runner labels, so nothing in your .github/workflows changes.
helm install gag \
oci://ghcr.io/actions-gateway/charts/actions-gateway \
--version 1.3.0 \
--namespace gmc-system --create-namespace \
--set gmc.image.digest=sha256:<gmc> \
--set agc.image.digest=sha256:<agc> \
--set proxy.image.digest=sha256:<proxy> \
--set wrapper.image.digest=sha256:<wrapper>
# v2 API (recommended): apply the signed, pre-rendered CRDs
kubectl apply --server-side -f \
https://github.com/actions-gateway/github-actions-gateway/releases/download/v1.3.0/actions-gateway-crds-v2.yaml
What GAG gives you¶
These ladder up one way: safe quotas and self-healing disruption make shared capacity usable, which is what lets you bin-pack expensive nodes and run on preemptible capacity. Estimate your savings vs ARC → · See every feature →
-
Tenant self-service under quotas
Quotas you can safely enforce:
- Platform-owned quota cap
- Blocked jobs auto-recover
- Zero manual reruns
- Self-serve
ActionsGateway, no ticket per change
-
No blocked critical jobs
Reserve capacity for key runners:
- Reserve N slots per runner type
- CPU tests can't starve GPU jobs
- Critical tests always schedule
-
No idle GPUs
Pods live only for the job:
- Created on acquire
- Deleted on completion
- GPU freed the instant a job ends
- Scale-to-zero by default
-
Isolated egress IPs
Each tenant's own proxy pool:
- Allow-list runners on Enterprise Managed Users
- Live-validated on GKE, 2026-07-13
- Stable IP needs a gateway or cloud NAT under it
- Share a pool across namespaces, by consent
- v2: proxy optional
-
Lower listener overhead
Listeners are goroutines, not pods:
- ~12 KiB per listener session
- One shared pod per tenant
- ARC: one always-on pod per set
-
Per-tenant observability
Scoped visibility, no cluster access:
- Prometheus per tenant + group
- Grafana dashboards + alerts, as code
- Job counts in
kubectl get - K8s Events on job transitions
- Cross-tenant fleet rollups for platform admins
-
Shared runner templates v2
Define once, reference by name:
RunnerTemplateper many sets- Platform
ClusterRunnerTemplate - Three validated templates ship in-box
- Identical templates collapse
- Migrate v1→v2 with
gag-migrate
-
Secure by default
Hardening reconciled by default:
baselinePod Security Admission per namespace- Default-deny NetworkPolicies
- Credentials never in env vars
- Workload identity keeps the App key out
- Signed images, SBOM, and SLSA provenance
- Kata micro-VM workers, proven in our own CI
-
Right-size from measured usage v2
No more guessed
resources:- Per-job usage peaks sampled
- Recommendations in
RunnerSetstatus - Opt-in profiles auto-apply at pod build
SizingDriftwarns; GPUs never touched
Who GAG is for¶
GAG targets a specific audience: teams that must self-host runners and run them for many tenants on one cluster. If that's you, here's the value per segment.
-
Platform & developer-experience teams
Multi-tenant CI on a shared cluster:
- Enforce a per-team quota without stranding jobs
- Tenants self-serve from one
ActionsGateway - No ticket queue for every runner change
-
Orgs that must self-host
Driven by a hard constraint, not preference:
- Compliance or data-residency requirements
- EMU or firewalled-service IP allow-lists
- Per-tenant egress IPs you allow-list directly
-
GPU / ML platform teams
Done paying for accelerators between jobs:
- Workers scale to zero, so no idle GPU
- GPU nodes return to the scheduler on completion
- Priority tiers keep critical GPU jobs scheduling
Not your setup? Three cases where something else wins, and we would rather say so.
- A vendor can run your jobs → a managed runner service, on speed and setup
- Managed Kubernetes is cheap, CI fits one cloud → a cluster per team isolates harder
- One team owns the cluster and the runners → ARC, whose protocol GAG is built on
GAG is for big, expensive nodes that several teams must share, safely. Compare the options →
How it fits together¶
A four-tier system: a cluster-scoped manager gives each tenant an isolated gateway from its ActionsGateway. Jobs are acquired with the same single-acquirer runner-scale-set protocol ARC uses, through the same actions/scaleset client library, and it is the shipped default. So runs-on keeps working and the protocol is not the difference. What differs is that the acquisition decision lives in the control plane rather than in the runner pod, which is what lets a job be declined before it is claimed.
Read the architecture overview for the full breakdown, jump to why GAG over ARC, browse every feature, or see the public roadmap for what's next.
What a tenant actually declares¶
The whole object set for a proxied gateway with a GPU runner set (priority
tiers) and a Linux runner set. Every resource is namespaced, none is
cluster-scoped, and no ResourceQuota field appears anywhere: the quota is
platform-owned and set on the namespace, so it is a cap the tenant cannot raise.
apiVersion: actions-gateway.com/v2beta1
kind: EgressProxy # (1)!
metadata:
name: team-a-egress
namespace: team-a
spec:
minReplicas: 2
maxReplicas: 10
---
apiVersion: actions-gateway.com/v2beta1
kind: RunnerTemplate # (2)!
metadata:
name: default
namespace: team-a
spec:
podTemplate:
spec:
containers:
- name: runner
---
apiVersion: actions-gateway.com/v2beta1
kind: ActionsGateway # (3)!
metadata:
name: team-a-gateway
namespace: team-a
spec:
credentials:
type: GitHubApp
githubApp:
name: my-github-app # name-only Secret ref in this namespace
githubURL: https://github.com/team-a-org
defaultProxyRef:
name: team-a-egress # every RunnerSet inherits this unless it sets proxyRef
---
apiVersion: actions-gateway.com/v2beta1
kind: RunnerSet
metadata:
name: gpu
namespace: team-a
spec:
gatewayRef: { name: team-a-gateway }
templateRef: { name: default } # (4)!
runnerLabels: ["gpu"] # (5)!
priorityTiers: # (6)!
- priorityClassName: runner-critical
threshold: 5
- priorityClassName: runner-standard
threshold: 20
---
apiVersion: actions-gateway.com/v2beta1
kind: RunnerSet
metadata:
name: linux
namespace: team-a
spec:
gatewayRef: { name: team-a-gateway }
templateRef: { name: default }
runnerLabels: ["linux"]
maxWorkers: 30
- Optional. A standalone per-tenant egress proxy pool, Horizontal Pod Autoscaler
(HPA)-managed between these bounds; all GitHub traffic exits through it on
dedicated IPs. Drop it (and
defaultProxyRef) for direct, stillNetworkPolicy-restricted egress, collapsing the minimum to three objects. - A reusable pod shape referenced by both
RunnerSets below viatemplateRef. Define it once; a cluster-scopedClusterRunnerTemplateshares one shape across every namespace. The Pod Security Admission (PSA) level is a namespace label in v2, not a CR field. All gateways in a namespace share one level. credentials.githubApp.namereferences aSecretin this namespace holding the GitHub AppappId,installationId, andprivateKey. The GMC watches the reference name, not the Secret contents. See credential rotation.WorkloadIdentityis the opt-in no-PEM credential member.- Both runner sets reference the same
RunnerTemplate. There is noResourceQuotafield on any of these CRs. The single quota every runner set shares is platform-owned, set on the namespace by the platform admin, so it is a real cap the tenant cannot raise. Priority tiers decide who wins when it is contended. - Exactly one label per runner set: it is the set's scale-set name at GitHub and
its single
runs-onmatch target (runs-on: gpu), unique across the sets under one gateway. A single-nameruns-oncarries over from ARC unchanged; a workflow targeting an array needs one edit per target, covered in migrating from ARC. - The first 5 GPU pods get the higher-priority
PriorityClass; the next tier bursts opportunistically; the final threshold caps total concurrency. ThepriorityClassNamevalues must be on the platform's allowlist (a watchedPriorityClassAllowlistCR, grown without a GMC restart; the--allowed-priority-classesflag remains the fail-safe baseline), and whether a tier preempts is set on the platform-ownedPriorityClassobject, so a tenant cannot name a class that evicts other tenants' pods.
The legacy single-CR v1alpha1 shape, which expresses this whole gateway in one
ActionsGateway CR, is still fully served but
deprecated, and removed at v2.0.0; see the
getting-started walkthrough
for it and the v1 → v2 migration guide to move
across without changing how your jobs are acquired.
Ready to try it? Follow the getting-started guide. Already running ARC? The Migrating from ARC guide maps every concept above onto GAG and walks one runner group across with zero downtime.