Skip to main content

Installing 3B with Helm

Written by Aron Day

A step-by-step guide to installing 3B on your own Kubernetes cluster from the oci.tines.com registry. This page covers the parts that are the same on every cluster.

The platform guides for AWS EKS, GCP GKE, and Oracle OKE don’t repeat these steps. They call out what each platform does differently and why 3B needs particular settings there, so you can fold that into how you already build clusters. Guides for Azure AKS and Scaleway Kubernetes Kapsule are coming soon; until then this page plus the node compatibility check below covers what 3B needs on any cluster.

Note: Self-hosted 3B is yours to run. We document what 3B requires and why it requires it. Provisioning the cluster, load balancer, TLS certificates, DNS, and backups stays with you, to your own platform standards. This guide assumes you are comfortable running production applications on Kubernetes.

How secrets are handled

The Helm chart generates every application secret itself on first install (db-credentials, auth-secrets, worker-secrets, api-secrets, credential-keys) and reuses them on upgrade, so nothing rotates under running data. The only things you create outside Helm are:

  • the namespace, and

  • the image pull secret (so pods can pull the private images).

You also log the Helm CLI in to the registry so it can pull the chart itself.

Prerequisites

Tooling

  • kubectl, with its context pointed at the target cluster.

  • Helm 3.8 or newer (OCI registry support).

  • Credentials for oci.tines.com — see accessing the OCI registry.

  • Your signed 3B license token (3bl_v1_…) from Tines.

Cluster

  • A default StorageClass — the chart requests persistent volumes for Postgres and the blobstore, which stay Pending without one.

  • User namespaces enabled on nodes — the api and worker pods run with hostUsers: false to isolate the gVisor sandbox, so they won’t schedule unless the node’s container runtime is containerd 2.x on a recent kernel and user.max_user_namespaces is non-zero. Step 5 verifies this. Some node images ship it hardened off; the platform guides say which, and why.

Step 1 — Set variables

export VERSION=<VERSION>                       # the release version from Tines, e.g. 2.0.0
export REG_USER='<oci.tines.com username>'
export REG_PASS='<oci.tines.com API key>'

Step 2 — Create the namespace

kubectl create namespace 3b

Step 3 — Create the image pull secret

The container images are served from the private oci.tines.com registry. The secret’s name must match imagePullSecrets in your values file — this guide uses tines-registry:

kubectl create secret docker-registry tines-registry \
  --namespace 3b \
  --docker-server=oci.tines.com \
  --docker-username="$REG_USER" \
  --docker-password="$REG_PASS"

Step 4 — Log Helm in to the registry

So the CLI can pull the chart:

echo "$REG_PASS" | helm registry login oci.tines.com \
  --username "$REG_USER" \
  --password-stdin

Step 5 — Verify node compatibility

Confirm the nodes can launch 3B’s sandbox before you install anything, with a throwaway pod that mimics how the api and worker pods run. It uses a 3B image and the pull secret from step 3, so it needs no registry access you don’t already have, and it doubles as a check that the pull secret works:

kubectl -n 3b apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: userns-probe
spec:
  hostUsers: false
  restartPolicy: Never
  imagePullSecrets:
    - name: tines-registry
  containers:
    - name: probe
      image: oci.tines.com/3b/nginx:$VERSION
      command: ["sh","-c","echo uid_map:; cat /proc/self/uid_map; sleep 5"]
EOF
kubectl -n 3b wait --for=condition=Ready pod/userns-probe --timeout=120s
kubectl -n 3b logs userns-probe
kubectl -n 3b delete pod userns-probe

The pod should reach Ready and print a non-identity uid_map, where the container’s 0 maps to a high host UID, for example 0 1000000 65536. Any image with a shell works here, so substitute one from your own registry if you prefer.

Two failure modes to know apart:

  • Stuck creating with a proc or rootfs mount error, or a uid_map of 0 0 4294967295, means the node image doesn’t support user namespaces. See your platform guide before continuing.

  • ImagePullBackOff means the pull secret or its credentials are wrong, which would fail the install too. Recreate it in step 3.

Step 6 — Configure your values

Create (or edit the provided) values.yaml with your real hostnames and license:

stackName: tines-3b-prod # a short, stable identifier for this deployment
license: "3bl_v1_…" # the signed license token from Tines

externalUrls:
  ui: https://3b.example.com
  api: https://3b-api.example.com
  public: https://3b-public.example.com
  zeroSync: https://3b-ws.example.com

cookies:
  domain: example.com # the shared parent domain of your hostnames
  secure: true

imagePullSecrets:
  - tines-registry

Notes:

  • Do not change stackName after the first install without migrating data — some stored data is scoped to it.

  • The UI, API, and Zero sync URLs may share a single hostname; the public URL needs its own. See connectivity requirements.

  • You do not need to configure images — the chart pins them to the correct release automatically. To inspect what a version pins:

helm show values oci://oci.tines.com/3b/charts/3b --version "$VERSION"

Step 7 — Install the chart

helm install 3b oci://oci.tines.com/3b/charts/3b \
  --version "$VERSION" \
  --namespace 3b \
  -f values.yaml \
  --timeout 15m

On first install the chart mints the five application secrets, runs the database migration job, then starts the services. Storage is provisioned automatically through the cluster’s default StorageClass. Watch it come up:

kubectl -n 3b get pods -w

Step 8 — Verify

# the five secrets the chart generated:
kubectl -n 3b get secret db-credentials auth-secrets worker-secrets api-secrets credential-keys

# the migration job finished, and the services are rolled out
# (api and worker running proves user namespaces work):
kubectl -n 3b get jobs
kubectl -n 3b rollout status deploy/api
kubectl -n 3b rollout status deploy/nginx
kubectl -n 3b get pods

Step 9 — Reach it

nginx is the front door and routes by hostname (see the system overview). Point your load balancer at the nginx service on port 80 and DNS for the hostnames in externalUrls at that load balancer. Because nginx host-routes internally, one load balancer covers all four hostnames.

Two requirements for whatever sits in front: it must allow long-lived websocket connections (raise its idle timeout well above the default) and, if it terminates TLS, forward X-Forwarded-Proto: https. See connectivity requirements and TLS termination. The platform guides for AWS EKS, GCP GKE, and Oracle OKE cover how each platform handles this.

Quick smoke test without DNS:

kubectl -n 3b port-forward svc/nginx 8080:80
curl -H 'Host: 3b.example.com' http://localhost:8080/

Then verify end to end:

  • Open the UI URL — it loads over HTTPS.

  • Sign in (the built-in identity provider is enabled by default).

  • Build a simple workflow and run a step to confirm end-to-end execution.

  • kubectl -n 3b get pods shows all pods Running / Completed with no restarts.

Upgrading

Same values, new version — the application secrets are read back and preserved, so no key or database-password rotation. See upgrading on Kubernetes.

Uninstalling

helm uninstall 3b -n 3b
# resources created outside the release remain; delete the namespace to purge
# everything, including the database and blob storage volumes:
kubectl delete namespace 3b

Appendix — External database (instead of in-cluster Postgres)

Set postgres.enabled: false in values.yaml, then create db-credentials yourself before install (the chart skips generating it when Postgres is disabled):

kubectl create secret generic db-credentials \
  --namespace 3b \
  --from-literal=password='<db-password>' \
  --from-literal=url='postgresql://user:pass@host:5432/3b' \
  --from-literal=host-url-zero-internal='postgresql://user:pass@host:5432/zero_internal' \
  --from-literal=zero-admin-password='<random>'

Your database must have logical replication enabled (wal_level=logical) for Zero sync.

Appendix — Optional integration secrets

Outbound email and AI provider keys live on the api-secrets secret (smtp-url, parallel-api-key, openai-global-api-key, openai-us-api-key, openai-eu-api-key). Set them at any time by patching the secret; values already stored are preserved across upgrades:

kubectl -n 3b patch secret api-secrets --type merge \
  -p '{"stringData":{"smtp-url":"smtps://user:[email protected]:465"}}'
kubectl -n 3b rollout restart deployment/api deployment/orchestrator deployment/public
Did this answer your question?