Install with Helm
The control plane, Git hosting and the dashboard in your Kubernetes cluster, with the data nodes installed as packages on VMs.
The chart installs the control plane into your cluster: cp-core as a one-replica StatefulSet, cp-public as a Deployment behind your Ingress, Git hosting, and the Jobs that run the same install core the packages run. The data nodes stay on VMs and join through a Service the chart exposes. This is profile cp-only, the supported shape; a full profile that also runs the nodes as a DaemonSet exists behind a measurement we have not finished, and the chart refuses it for production.
Prerequisites
- Kubernetes 1.30 or newer. The chart's zero-downtime upgrade of cp-public needs the
sleeppreStop handler, which is 1.30+. - An Ingress controller and a TLS Secret for the dashboard and Git names.
- A StorageClass for two small ReadWriteOnce volumes: cp-core's state and the Git data.
- A CNI that enforces NetworkPolicy, if you want the policy the chart ships to mean anything. The chart cannot check this; measured on Calico.
- A PostgreSQL 18 and an S3-compatible store reachable from the cluster, as in Requirements.
- The data-node VMs on a network the cluster's LoadBalancer or NodePort reaches.
Add the repository
helm repo add runlot https://charts.runlot.io
helm repo update
helm search repo runlot --develChart versions follow runlot's release tags one to one. While every runlot release is a release candidate, --devel is needed to see them.
The Secrets you create
kubectl create namespace runlot
kubectl -n runlot create secret generic runlot-master-key --from-literal=masterKey="$(openssl rand -hex 32)"
kubectl -n runlot create secret generic runlot-license --from-file=license=./license
kubectl -n runlot create secret generic runlot-meta-db --from-literal=url='postgres://runlot:…@db.acme.example:5432/runlot_meta?sslmode=require'
kubectl -n runlot create secret generic runlot-s3 --from-literal=accessKeyId=… --from-literal=secretAccessKey=…
kubectl -n runlot create secret generic runlot-smtp --from-literal=user=… --from-literal=password=…The chart never creates the master key or the licence Secret. We have no copy of the key, and the chart refusing to generate one is how that stays true in a cluster too.
values.yaml
profile: cp-only
tier: single # eval | single | ha
license: { existingSecret: runlot-license }
masterKey: { existingSecret: runlot-master-key }
metaDB: { existingSecret: runlot-meta-db }
objectStore:
endpoint: https://s3.acme.example
bucket: runlot
pathStyle: true
existingSecret: runlot-s3
domains:
dashboard: runlot.acme.example
git: git.acme.example
apps: apps.acme.example
wire: wire.acme.example
objects: objects.acme.example
org: { name: acme, adminEmails: [[email protected]] }
mail: { smtp: { host: smtp.acme.example, port: 587, existingSecret: runlot-smtp, from: [email protected] } }
ingress:
className: nginx
tls: { dashboardSecret: runlot-dash-tls, gitSecret: runlot-git-tls }
service:
cpCoreNode: { type: LoadBalancer } # what the VM nodes reach the control plane throughhelm show values runlot/runlot --devel prints the annotated full set. values.schema.json refuses a wrong combination at helm lint rather than after a successful deploy: a node block under cp-only, eval settings outside tier: eval, or an empty master-key or licence Secret outside eval.
Install
helm install runlot runlot/runlot --devel -n runlot -f values.yamlThe order the chart runs: RBAC and its own two Secrets, then a Job running preflight and install cp, then cp-core, cp-public, Git, and a post-install Job that writes the node join token into the Secret runlot-node-join.
kubectl -n runlot get secret runlot-node-join -o jsonpath='{.data.token}' | base64 -dGive the node Service an address the VMs can reach
The VM nodes reach cp-core on a mutually authenticated TLS port. Once the LoadBalancer has an address, that address has to be in cp-core's certificate:
kubectl -n runlot get svc runlot-cp-core-node -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
helm upgrade runlot runlot/runlot --devel -n runlot -f values.yaml \
--set service.cpCoreNode.externalAddress=<that address>The chart's NOTES print this line after the first install, because the address exists only afterwards.
Install the nodes as packages
Follow Install with packages: each data node, with RUNLOT_CP_PRIVATE_ADDR set to the Service's external address and the join token from the Secret above. If the chart's NetworkPolicy is enforced in your cluster, the VM nodes' addresses have to be in networkPolicy.nodeCIDRs, and so does your cluster's node subnet: a LoadBalancer SNATs to it.
Declare where the nodes are
kubectl -n runlot exec deploy/runlot-cp-public -- \
runlot-admin bootstrap --core http://runlot-cp-core:8081 --nodes node-1=icn/KR/doUnder cp-only the nodes join after the chart is up, so the post-install Job has nothing to declare on and says so. This command is how the fact is set.
Upgrade
helm repo update
helm upgrade runlot runlot/runlot --devel -n runlot -f values.yaml --version <new>The pre-upgrade Job runs the schema migrations before the new control plane starts. cp-public rolls with no dropped requests; cp-core is one replica and restarts, during which the dashboard shows the last state and the nodes keep serving. Then upgrade the node packages as in Operate: upgrades.
Changing the licence
kubectl -n runlot create secret generic runlot-license --from-file=license=./license --dry-run=client -o yaml | kubectl apply -f -The control plane re-reads the mounted Secret within a minute. No restart.