Install self-hosted Gauntlet on Kubernetes
One Helm release. It runs on Community (free: 3 environments, 3 tests at a time) until you add a license.
On this page
You need
| Cluster | Kubernetes 1.28 or newer, Helm 3, a default StorageClass (or set persistence.storageClass and mongo.persistence.storageClass). |
|---|---|
| Permissions |
By default the platform's ServiceAccount is cluster-admin: it creates a namespace, quota and network policy per environment, on the fly.
rbac.clusterAdmin: false uses a ClusterRole with only what it needs (still cluster-wide; no CRDs, webhooks, cluster RBAC or nodes).
On clusters without a HelmChart API (EKS, GKE, AKS) the chart also runs the k3s helm-controller for Helm-chart apps; it is always cluster-admin.
|
| Capacity |
Platform 0.1 CPU, 128–512 MiB. Bundled MongoDB 0.1 CPU, 256 MiB–1 GiB.
Each environment: about 4 GiB memory for a Helm-chart app (its virtual cluster), within a per-environment quota of 6 CPU / 10 GiB requested, 16 GiB limit, 40 pods (environments.quota.*).
Each running test: 0.5 CPU, 1–3 GiB.
|
| Storage | 5 GiB for MongoDB, 10 GiB for screenshots and recordings (two PVCs). |
| Sign-in | An OpenID Connect provider: GitLab, Google, Okta, Keycloak… |
| Model | An Anthropic API key, or an Anthropic-compatible AI gateway, or neither (then everyone adds their own key in the console). |
| Network in | An ingress or load balancer for publicUrl, with TLS (ingress.*, or your own in front of the Service gauntlet, port 8090). |
| Network out | Your OIDC issuer; the model API or gateway; the license server (not when air-gapped); image registries (ghcr.io, docker.io, yours); https://charts.loft.sh (virtual clusters) and the chart repositories your blueprints use. |
| Preview links | Optional: wildcard DNS and certificate, see Preview links. |
1. Register Gauntlet with your sign-in provider
GitLab: instance-wide under Admin → Applications → Add new application, or for one group under Group → Settings → Applications.
| Name | Gauntlet |
|---|---|
| Redirect URI | <publicUrl>/auth/callback, e.g. https://gauntlet.acme.com/auth/callback |
| Trusted | On (no consent screen; instance applications only) |
| Confidential | On |
| Scopes | openid email profile |
Then oidc.issuer = your GitLab URL, oidc.clientId = the Application ID, client secret = the Secret.
Other providers: the same redirect URI and scopes; the issuer is the URL that serves /.well-known/openid-configuration.
Limit who can sign in: oidc.allowedDomains (email domains) or oidc.allowedGroups (the provider's groups claim; on GitLab, group paths such as acme/platform).
2. Write your values
Required: publicUrl, oidc.issuer, oidc.clientId and the client secret. Keep secrets in a Secret you manage:
kubectl create namespace gauntlet
kubectl -n gauntlet create secret generic gauntlet-secrets \
--from-literal=oidc-client-secret='<client secret>' \
--from-literal=llm-api-key='<gateway key>' \
--from-literal=cookie-secret="$(openssl rand -base64 32)" \
--from-literal=secrets-key="$(openssl rand -base64 32)" \
--from-literal=mongo-root-password="$(openssl rand -hex 24)"# values.yaml
image: ghcr.io/gauntlet-dev/gauntlet:1.0.0
runnerImage: ghcr.io/gauntlet-dev/gauntlet-runner:1.0.0
publicUrl: https://gauntlet.acme.com
oidc:
issuer: https://gitlab.acme.com
clientId: <application id>
# optional:
allowedGroups: [acme]
# the Secret above
existingSecret: gauntlet-secrets
# installation admins (none: a one-time bootstrap token, see below)
adminEmails: [dana@acme.com]
# where licenses are renewed
licenseServer: https://usegauntlet.dev
# an AI gateway; or anthropicApiKey (Secret key anthropic-api-key)
llm:
baseUrl: https://ai-gateway.acme.com
ingress:
enabled: true
className: nginx
tls: [{ secretName: gauntlet-tls, hosts: [gauntlet.acme.com] }]
# license: paste it in the console later (or the Secret key license)
# mongo.uri: your own MongoDB instead of the bundled one (or the Secret key mongo-uri)existingSecret names one Secret; keys you leave out fall back to the values. Keys: oidc-client-secret license anthropic-api-key claude-oauth-token llm-api-key smtp-password bootstrap-token mongo-uri cookie-secret secrets-key mongo-root-password.
GitOps (Argo CD, Flux): put cookie-secret, secrets-key and mongo-root-password there. Otherwise the chart generates them with lookup, which those tools don't run, and every sync rotates them.
3. Install
# chart 1.0.0
curl -fsSLo gauntlet-chart.tgz https://usegauntlet.dev/download/gauntlet-chart.tgzhelm install gauntlet ./gauntlet-chart.tgz -n gauntlet -f values.yaml --wait
# checks the console, MongoDB and OIDC discovery
helm test gauntlet -n gauntlet
# the version you run
curl -fsS https://gauntlet.acme.com/api/versionThe install output repeats the callback URL to register and how the first admin signs in. Open publicUrl and sign in.
4. First admin
adminEmailsset: those people are installation admins (license, users) when they sign in.- Not set: the server prints a one-time bootstrap token in its log. Whoever signs in with it becomes the first admin; once there is an admin it never works again.
To choose it yourself:kubectl -n gauntlet logs deploy/gauntlet | grep -i bootstrap # then open https://gauntlet.acme.com/auth/login?bootstrap=<token>bootstrapToken, or the Secret keybootstrap-token.
5. License
Buy one on the pricing page; it's emailed and kept in your license account. Install it on the console's License page, <your Gauntlet URL>/settings/license (paste; takes effect at once), or as the Helm value license / Secret key license. A license installed in the console wins over the Helm one.
| Validity | Each license is valid for about 30 days (Valid until on the License page). The installation fetches a fresh one from the license server automatically: normally nothing to do. |
|---|---|
| Air-gapped | Set licenseRefresh: "off" (no calls out) and paste a fresh license from your license account before Valid until. |
| Installation ID | On the License page. Give it when you buy, or later in the license account; a license without one binds to the first installation that renews it. Stored in MongoDB: a reinstall that keeps the data keeps the ID. |
| Move it | License account → Use on another installation. The old installation gets no more renewals and drops to Community when its current license runs out. |
| Subscription ends | 14 days of grace, then Community. Nothing is deleted. |
Terms: license terms, terms of service, privacy.
6. Email (optional)
smtp:
host: smtp.acme.com
port: 587
username: gauntlet
from: "Gauntlet <gauntlet@acme.com>"
# starttls | tls | none
tls: starttls
# password: the Secret key smtp-password (or smtp.password)
For invites and license warnings. Without smtp.host no email is sent: the console shows the invite link to copy and says so.
7. Preview links (optional)
Each environment's services get links like https://web--<env>.preview.acme.com. Set previewDomain: preview.acme.com, point wildcard DNS *.preview.acme.com at the ingress, and add a wildcard certificate to ingress.tls for https. With ingress.enabled the chart routes *.previewDomain too.
Upgrade
helm upgrade gauntlet ./gauntlet-chart-<version>.tgz -n gauntlet -f values.yaml --wait
curl -fsS https://gauntlet.acme.com/api/version
- Always pass your values: without
-f values.yamlHelm stops on the missing required values. - One replica, replaced in place: the console is down during the restart. Data and running environments are kept.
- Back up first (below). Then everyone updates their CLI:
curl -fsSL https://gauntlet.acme.com/api/cli/install.sh | sh.gauntlet versionshows CLI and server.
Back up and restore
| MongoDB | Everything: users, projects, runs, findings, stored model keys (encrypted), license, installation ID. |
|---|---|
PVC gauntlet-data | Screenshots, recordings, logs. |
Secret gauntlet (or your existingSecret) | secrets-key decrypts stored model keys: without it they must be added again. cookie-secret keeps people signed in. |
# bundled MongoDB
kubectl -n gauntlet exec deploy/gauntlet-mongo -- sh -c \
'mongodump --archive --gzip -u root -p "$MONGO_INITDB_ROOT_PASSWORD" --authenticationDatabase admin' > gauntlet-mongo.gz
# artifacts and secrets
kubectl -n gauntlet exec deploy/gauntlet -- tar czf - -C /data . > gauntlet-data.tgz
kubectl -n gauntlet get secret gauntlet -o yaml > gauntlet-secret.yaml
Restore: install with the same secrets (existingSecret, or kubectl apply -f gauntlet-secret.yaml first), then:
kubectl -n gauntlet scale deploy/gauntlet --replicas=0
kubectl -n gauntlet exec -i deploy/gauntlet-mongo -- sh -c \
'mongorestore --archive --gzip --drop -u root -p "$MONGO_INITDB_ROOT_PASSWORD" --authenticationDatabase admin' < gauntlet-mongo.gz
kubectl -n gauntlet scale deploy/gauntlet --replicas=1
kubectl -n gauntlet exec -i deploy/gauntlet -- tar xzf - -C /data < gauntlet-data.tgz
External MongoDB (mongo.uri): use your database's own backups.
Uninstall
helm uninstall gauntlet -n gauntlet
Kept on purpose, so a reinstall picks up where you left off: the PVCs gauntlet-mongo and gauntlet-data, the Secret gauntlet and the Secret gauntlet-installation (your installation ID, so licenses keep working after a reinstall; delete it to get a new ID). Also left behind: running environments (namespaces labelled app.kubernetes.io/managed-by=gauntlet; a reinstall on the kept data manages them again, and removes those it no longer knows after 10 minutes), the namespace gauntlet-system and the helm.cattle.io CRDs. To remove everything:
# environments (better: stop them in the console first)
kubectl delete ns -l app.kubernetes.io/managed-by=gauntlet
kubectl -n gauntlet delete pvc gauntlet-mongo gauntlet-data
# also deletes the Secrets gauntlet and gauntlet-installation
kubectl delete ns gauntlet-system gauntlet
# only if nothing else uses them (k3s and RKE2 ship them built in):
kubectl delete crd helmcharts.helm.cattle.io helmchartconfigs.helm.cattle.io
Replicas
Gauntlet runs as one replica (Recreate updates). The reconciler holds a Kubernetes Lease (leader election), so a second pod started by mistake doesn't manage environments twice. Availability comes from quick restarts and backups; for a replicated database use your own MongoDB (mongo.uri).
Security model
| What | Where and how |
|---|---|
| Test environments | A new namespace env-<id> per environment, on your cluster's nodes, running the app built from the project's branch. |
| Agent runner | The AI agent and its browser, in the environment's namespace. Its kubeconfig is admin of that namespace only (or its vcluster) and expires with the run. Model calls go through the platform's proxy with a run-scoped token: your model key never enters the environment. |
| Runtime | The cluster default (usually runc, a shared kernel). For untrusted code set runtimeClass to a gVisor or Kata RuntimeClass; it applies to the runner and container apps, not to Helm-chart apps (their pods run inside a virtual cluster on the default runtime). Give that RuntimeClass scheduling.nodeSelector to keep them on dedicated nodes. |
| Network | Each environment gets a NetworkPolicy: in only from itself and the platform; out only to DNS, itself, the platform API and the internet. Private ranges and cloud metadata are blocked, so environments can't reach MongoDB, other namespaces or each other. Your CNI must enforce NetworkPolicies. |
| Limits | Pod Security baseline on environment namespaces; a ResourceQuota (environments.quota.*) with no LoadBalancer or NodePort Services. |
| Platform RBAC | Cluster-wide, to create and delete environment namespaces and their contents. cluster-admin by default; rbac.clusterAdmin: false uses a narrower ClusterRole (listed in the chart README). The optional helm-controller is always cluster-admin. |
Troubleshooting
kubectl -n gauntlet get pods
kubectl -n gauntlet logs deploy/gauntlet --tail=100
helm test gauntlet -n gauntlet
| Symptom | Check |
|---|---|
Pod not ready (/readyz fails) | The log says what is down: MongoDB or OIDC discovery. /livez stays up, so an outage doesn't restart the pod. |
| Sign-in fails | Redirect URI exactly <publicUrl>/auth/callback; client ID and secret; the issuer's /.well-known/openid-configuration reachable from the cluster; oidc.allowedDomains / allowedGroups. |
| Signed out again right away | The session cookie is Secure when publicUrl is https: TLS must terminate in front of Gauntlet. |
| Changed a secret, nothing happened | Values: helm upgrade … -f values.yaml restarts the pod. A Secret you manage: kubectl -n gauntlet rollout restart deploy/gauntlet. |
| Runs fail: AI gateway unreachable | llm.baseUrl from inside the cluster, the key llm-api-key, egress policies. |
| Environments wait for room | Cluster capacity vs environments.quota.*, or the plan's environment limit (/settings/license). |
| Preview links don't open | previewDomain, wildcard DNS and certificate. |
| Monitoring | Prometheus metrics at /metrics (set GAUNTLET_METRICS_TOKEN under extraEnv to require a bearer token); probes /livez and /readyz; version at /api/version. |
Values reference
The chart's README lists every value, the RBAC rules and what uninstall leaves: helm show readme ./gauntlet-chart.tgz. Defaults and comments: helm show values ./gauntlet-chart.tgz.