Skip to content
runlot

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 sleep preStop 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

Terminal
helm repo add runlot https://charts.runlot.io
helm repo update
helm search repo runlot --devel

Chart 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

Terminal
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

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 through

helm 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

Terminal
helm install runlot runlot/runlot --devel -n runlot -f values.yaml

The 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.

Terminal
kubectl -n runlot get secret runlot-node-join -o jsonpath='{.data.token}' | base64 -d

Give 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:

Terminal
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

Terminal
kubectl -n runlot exec deploy/runlot-cp-public -- \
  runlot-admin bootstrap --core http://runlot-cp-core:8081 --nodes node-1=icn/KR/do

Under 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

Terminal
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

Terminal
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.

On this page