Install on Kubernetes
Install GoRunner in your cluster with the Helm chart, on your own database, Redis and S3.
This page is for self-hosted installations. On GoRunner Cloud, GoRunner runs this for you.
You need
- A cluster and your own stores
- Kubernetes 1.29 or newer, with
kubectland Helm 3.8 or newer on your computer. An ingress controller with certificates (such as cert-manager), and a network plugin that enforces NetworkPolicy. PostgreSQL 15 or newer and Redis 6.2 or newer, set never to delete keys; an S3 bucket is optional. What each must be. - A domain name
- Such as
automations.example.com. You point it at your ingress controller in step 4. - Access to GoRunner’s images
- A GitHub account GoRunner gave access to, and a read-only token for it, as in Install with Docker. Your license comes from GoRunner too: ask at gorunner.io/contact, free or paid.
1.Make a namespace
The chart and the image are private for now. Sign Helm in with that GitHub account’s user name, and paste the token when it asks. Then make the namespace and give it the same user name and token, so every pod can download the image.
A registry mirror inside your company? Use its address in place of ghcr.io, and set image.repository to its copy of gorunner-selfhost in step 3.
helm registry login ghcr.io -u <user name>
kubectl create namespace gorunner
kubectl --namespace gorunner \
create secret docker-registry ghcr \
--docker-server=ghcr.io \
--docker-username=<user name> \
--docker-password=<token>2.Make the secrets
Passwords and keys go in one Secret, its keys named as the settings. The file keeps them out of your shell history.
DATABASE_URL,REDIS_URL- Your database and Redis. Use
rediss://when Redis needs TLS. S3_…- Your bucket’s key. Leave both out with no bucket.
SMTP_PASSWORD- Or
RESEND_API_KEY. Leave both out with no mail.
Danger ENCRYPTION_KEY opens every password and connection GoRunner stores. Copy the file into your password manager now, then delete it. Lose the key, or change it, and nothing stored can be read again.
Use Sealed Secrets, Vault or separate fields?
Any Secret works the same: give its name as existingSecret.
For separate fields, put only POSTGRES_PASSWORD and REDIS_PASSWORD in the Secret, and the hosts and the database user in your values (postgresql.host, postgresql.user, redis.host). Then no character in a password needs escaping.
The chart can also write the Secret itself from values, but then the secrets live in your values file. See Helm values.
umask 077
cat > gorunner-secrets.env <<'EOF'
DATABASE_URL=postgresql://gorunner:<password>@db.example.internal:5432/gorunner?sslmode=require
REDIS_URL=rediss://:<password>@redis.example.internal:6379/0
S3_ACCESS_KEY=<access key>
S3_SECRET_KEY=<secret key>
SMTP_PASSWORD=<password>
EOF
echo "ENCRYPTION_KEY=$(openssl rand -hex 32)" \
>> gorunner-secrets.env
kubectl --namespace gorunner \
create secret generic gorunner-secrets \
--from-env-file=gorunner-secrets.env3.Write your values
Everything else goes in a values file. This one suits ingress-nginx with cert-manager.
admin.email- The only address that can make the first account. With
existingSecret, set it here or asADMIN_EMAILin the Secret: without either, nobody can, unless you setadmin.allowOpenFirstSignup: true. ingress.host- The address people open. With
tls.enabled, it is https. mailsmtporresend. Leave it out for no mail: people then sign in with a password.objectStorage- Your bucket.
mode: nonekeeps step data in the database.
Every value is in Helm values.
existingSecret: gorunner-secrets
imagePullSecrets:
- name: ghcr
admin:
email: admin@example.com
ingress:
className: nginx
host: automations.example.com
tls:
enabled: true
secretName: gorunner-tls
annotations:
cert-manager.io/cluster-issuer: letsencrypt
mail:
backend: smtp
from: "GoRunner <no-reply@example.com>"
smtp:
host: smtp.example.com
port: 587
user: apikey
objectStorage:
mode: external
bucket: gorunner
region: eu-west-1
external:
endpoint: https://s3.eu-west-1.amazonaws.com4.Install
Name the version without its leading v. Helm runs the migrations first, with the preflight check. The app and the workers start when they finish.
You should see after a minute or two, every pod is Running and 1/1, and the migrations’ pod is Completed.
Then helm test checks the dashboard, readiness and the embed’s rules. It ends Phase: Succeeded.
Once kubectl --namespace gorunner get ingress shows an address, point your domain name at it.
helm install gorunner \
oci://ghcr.io/ralve-org/charts/gorunner \
--version 2026.10.30 --namespace gorunner \
-f values.yamlGoRunner v2026.10.30 is installing as "gorunner" in namespace "gorunner".kubectl --namespace gorunner get pods --watch
helm test gorunner --namespace gorunner
kubectl --namespace gorunner get ingress5.Create the admin account
Read the setup code from the app. Only someone who can reach the cluster has it.
Open your address, enter the code, and create the admin account. First sign-in takes it from there.
kubectl --namespace gorunner \
exec deploy/gorunner-api -- gorunner setup-codeK7QM-4RZPThe ingress
Your ingress controller ends https and passes every request to the app’s pods. Whichever you run, it must:
- Stream answers
- No response buffering. The live run view and the embed are streams.
- Wait long
- A read timeout of at least 60 seconds; an hour is safe. GoRunner sends a keep-alive every 25 seconds.
- Take 6 MiB
- Request bodies up to 6 MiB. GoRunner takes up to 5 MiB.
- Add no security headers
- No
X-Frame-OptionsorContent-Security-Policyof its own. GoRunner sets its own on every page; the embed needs them. - See the caller
- Put the controller’s Service on
externalTrafficPolicy: Local, or use the PROXY protocol. Otherwise every caller shares one set of limits.
The chart’s network policy lets only your controller reach the app. It admits the ingress-nginx namespace by default; for any other controller, change networkPolicy.apiFrom, or the app answers nobody.
ingress:
className: nginx
# nginxDefaults: true (the default) adds these annotations:
# nginx.ingress.kubernetes.io/proxy-buffering: "off"
# nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
# nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
# nginx.ingress.kubernetes.io/proxy-body-size: 6mingress:
className: traefik
nginxDefaults: false
networkPolicy:
apiFrom:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: traefikingress:
className: alb
nginxDefaults: false
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
alb.ingress.kubernetes.io/certificate-arn: <your certificate's ARN>
alb.ingress.kubernetes.io/healthcheck-path: /api/ready
alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds=3600
api:
trustedProxies: "10.0.1.0/24 10.0.2.0/24"
networkPolicy:
apiFrom:
- ipBlock:
cidr: 10.0.1.0/24
- ipBlock:
cidr: 10.0.2.0/24- ingress-nginx
- Nothing to add: with
className: nginx, the chart sets the four annotations above. Its maintainers retired it in March 2026; it still works. - Traefik
- Use the namespace Traefik runs in (
kube-systemon k3s). Add nobufferingmiddleware, and keep the entry point’swriteTimeoutat0or over an hour. - AWS Load Balancer Controller
- With
target-type: ip, the load balancer reaches the pods from its own subnets, so the trusted ranges and the network policy name those subnets. Use your own.
Who does GoRunner believe about the caller’s address?
Left empty, api.trustedProxies trusts every private address. That is safe only because the network policy lets nothing but your controller reach the app, so your network plugin must enforce it. helm test checks that only when the policy closes something to it: a bundled store, or telemetry.metrics.enabled. Otherwise it prints skip NetworkPolicy enforcement.
Turn the policy off, and api.trustedProxies must name your controller’s own addresses. See The address ranges to trust.
Can I try it without stores of my own?
Yes, on a test cluster. The chart runs its own PostgreSQL, Redis and SeaweedFS, and makes their passwords. Leave DATABASE_URL, REDIS_URL and the S3 keys out of the Secret.
postgresql:
bundled:
enabled: true
redis:
bundled:
enabled: true
objectStorage:
mode: bundledEach is one pod with one volume: no backups, no failover. Its pods carry gorunner.io/evaluation-only: "true". The only way onto your own stores later is a dump and restore.
Next
- First sign-inThe admin account, your license and two-factor · 5 min
- Helm valuesEvery value, with its default
- Updatehelm upgrade, and going back · 5 min
Checked against 12d85b3b