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
Pendingwithout one.User namespaces enabled on nodes — the
apiandworkerpods run withhostUsers: falseto isolate the gVisor sandbox, so they won’t schedule unless the node’s container runtime is containerd 2.x on a recent kernel anduser.max_user_namespacesis 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
procorrootfsmount error, or auid_mapof0 0 4294967295, means the node image doesn’t support user namespaces. See your platform guide before continuing.ImagePullBackOffmeans 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
stackNameafter 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 podsshows all podsRunning/Completedwith 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
