Kubernetes Deployment
This guide deploys a full Bridge cluster on Kubernetes. It uses the same orchestrator / proxy / session roles as the AWS deployment, but the orchestrator creates workers as Kubernetes pods instead of ECS tasks. It works on managed Kubernetes (EKS, GKE, AKS) and on self-managed clusters.
What is Kubernetes? Kubernetes runs and manages containers across a group of
machines. You describe what you want in YAML “manifests” and apply them with
kubectl; Kubernetes makes it so. If you already run apps on Kubernetes, the
Bridge fits right in.
Overview
You’ll create resources in a dedicated bridge namespace:
- A PostgreSQL database (a pod for testing, or point at a managed database for production).
- A ConfigMap holding the Bridge configuration.
- A Secret holding the encryption key and other secrets.
- The orchestrator Deployment, with a ServiceAccount and permission to manage pods.
- A Service that exposes the proxy pods to users.
The orchestrator creates the proxy and session pods itself - you don’t define them as static Deployments.
Architecture
This diagram follows the Helm chart and the downloadable
bridge-cluster-k8s.yaml manifest. Kubernetes provides the scheduling, Service
routing, and RBAC; Bridge’s orchestrator owns the proxy and session worker
lifecycle.
flowchart LR
Clients["Approved clients<br/>(browser + native tools)"]
Britive["Britive platform<br/>(optional broker connection)"]
Targets["Target networks<br/>(services, nodes, databases, devices)"]
subgraph Cluster["Kubernetes cluster"]
subgraph Namespace["bridge namespace"]
ProxySvc["bridge-proxy Service<br/>LoadBalancer / NodePort / L4 edge"]
K8sApi["Kubernetes API<br/>pods create/watch/delete"]
subgraph Control["Control plane resources"]
Config["ConfigMap<br/>Bridge config"]
Secrets["Secret<br/>cluster token + encryption key"]
RBAC["ServiceAccount + RoleBinding"]
end
Orchestrator["Orchestrator pod<br/>migrations + reconciler<br/>optional co-located broker"]
ProxyPods["Proxy pods<br/>spawned by orchestrator"]
SessionPods["Session pods<br/>spawned by orchestrator"]
Postgres[("PostgreSQL<br/>managed DB or bundled pod")]
Recordings[("Shared recordings volume<br/>RWX PVC / EFS / Azure Files / NFS")]
end
end
Clients -->|"HTTPS / native protocols"| ProxySvc
ProxySvc -->|"ready proxy endpoints"| ProxyPods
ProxyPods -->|"session control + relay"| SessionPods
SessionPods -->|"protocol connection"| Targets
Orchestrator -->|"uses RBAC"| K8sApi
K8sApi -->|"creates / retires"| ProxyPods
K8sApi -->|"creates / retires"| SessionPods
Config --> Orchestrator
Secrets --> Orchestrator
RBAC --> Orchestrator
Orchestrator --> Postgres
ProxyPods --> Postgres
SessionPods --> Postgres
ProxyPods --> Recordings
SessionPods --> Recordings
Orchestrator --> Recordings
Orchestrator <-->|"outbound TLS when broker is enabled"| Britive
You can deploy with Helm, or use the downloadable Kubernetes manifest below if
you want to inspect and tune each resource by hand. The only external runtime
artifact you need is the public britive/bridge container image.
Install with Helm
If you use Helm or Rancher (whose Apps catalog is Helm-based), install the Bridge chart from Docker Hub as an OCI chart:
export BRIDGE_CLUSTER_TOKEN="$(openssl rand -hex 32)"
export BRIDGE_ENCRYPTION_KEY_B64="$(openssl rand -base64 32)"
export BRIDGE_HOST_KEY_SEED="$(openssl rand -hex 16)"
helm install bridge oci://registry-1.docker.io/britive/bridge-chart \
--version 2.0.0 \
-n bridge --create-namespace \
--set recordings.pvc.storageClassName="<ReadWriteMany-capable class>" \
--set secrets.clusterToken="$BRIDGE_CLUSTER_TOKEN" \
--set secrets.encryptionKeyB64="$BRIDGE_ENCRYPTION_KEY_B64" \
--set secrets.hostKeySeed="$BRIDGE_HOST_KEY_SEED" \
--set config.auth.britive.tenant="your-tenant"recordings.pvc.storageClassName has no default, and the chart refuses to render
without it. The recordings volume is mounted by the orchestrator and by every
session and proxy pod it spawns, on whatever node each lands on, so it needs a
ReadWriteMany StorageClass: efs-sc on EKS, azurefile on AKS, an NFS class
on-prem. A cluster’s default StorageClass is usually ReadWriteOnce-only, which would
leave the claim Pending and the orchestrator unschedulable.
For a single-node evaluation such as Docker Desktop or a one-node kind cluster, use a host path instead:
--set recordings.mode=hostPath --set orchestrator.replicas=1To enable the co-located broker and automatic online license retrieval, also set the broker-pool token:
helm upgrade --install bridge oci://registry-1.docker.io/britive/bridge-chart \
--version 2.0.0 \
-n bridge --create-namespace \
--reuse-values \
--set secrets.brokerAuthToken="<broker-pool-token>"Configure runtime worker pods
The orchestrator creates session and proxy pods at runtime, so their pod settings
come from worker.*, not from a Helm Deployment template.
| Values | Purpose |
|---|---|
worker.resources | CPU and memory requests and limits for every worker. |
worker.podSecurityContext and worker.securityContext | Non-root identity, seccomp, privilege escalation, and Linux capabilities. The defaults satisfy the restricted Pod Security Standard when recordings use a PVC. |
worker.nodeSelector, worker.tolerations, worker.affinity, and worker.priorityClassName | Scheduling rules for workers, independent of the orchestrator. |
worker.serviceAccount.* | A dedicated worker service account. Use its annotations for EKS, GKE, or other workload identity without granting that identity to the orchestrator. |
Show every value before you build a production values file:
helm show values oci://registry-1.docker.io/britive/bridge-chart --version 2.0.0Worker pods are scoped to the Helm release and carry an owner reference to its
ConfigMap. helm uninstall therefore removes them. A Helm upgrade leaves running
workers in place. The orchestrator replaces them when their image changes or when
they reach the configured session limits and lifespans. A configuration-only
change reaches a warm Kubernetes worker at its next normal cycle.
The default security contexts are compatible with the restricted Pod Security
Standard only when recordings.mode=pvc. Kubernetes forbids hostPath volumes
under both baseline and restricted, and Bridge needs a root init container to
make its host path writable.
Upgrading from chart 1.x requires one cleanup step. Workers created by chart 1.x do not have the release label or owner reference added in chart 2.0.0. Wait for any old session workers to finish, then remove the old pool after the new proxies are ready:
kubectl delete pods -n <namespace> -l 'app=bridge,bridge.role,!app.kubernetes.io/instance'For production, point datastore.* at a managed PostgreSQL database, configure
shared ReadWriteMany storage for recordings, pin a tested Bridge image tag, and
front the proxy Service with an Ingress or cloud load balancer that terminates
TLS. In Rancher, add the same OCI chart URL to Apps and set the equivalent values
in the install form.
The rest of this guide walks the raw-manifest path, which is useful when you want to read or hand-tune every Kubernetes resource.
Before You Begin
You need:
- A working Kubernetes cluster and
kubectlconfigured to reach it (kubectl get nodesshould list your nodes). - The starting manifest, downloaded here: bridge-cluster-k8s.yaml. You’ll customize it below.
- For production: a way to provide shared storage for recordings that every pod can read and write (a ReadWriteMany volume such as EFS, Azure Files, or NFS), and ideally a managed PostgreSQL database.
You don’t build anything. The Bridge image is published on Docker Hub as
britive/bridge and the manifest already references britive/bridge:latest. As
long as your cluster’s nodes can reach Docker Hub, they pull it automatically.
Deploy with the Manifest
Make the Image Reachable
The manifest you downloaded uses britive/bridge:latest from Docker Hub. If
your cluster nodes have internet access, there’s nothing to do - skip to Step 2.
Air-gapped or private-registry clusters: mirror the image into a registry your
nodes can reach, then update the image: fields in the manifest to match.
docker pull britive/bridge:latest
docker tag britive/bridge:latest myregistry.example.com/bridge:latest
docker push myregistry.example.com/bridge:latestGenerate the Secrets
echo "ENCRYPTION_KEY_B64: $(openssl rand -base64 32)" # encrypts stored credentials
echo "CLUSTER_TOKEN: $(openssl rand -hex 32)" # secures internal cluster calls
echo "HOST_KEY_SEED: $(openssl rand -hex 16)" # stable SSH fingerprint across workersCustomize the Manifest
Open the manifest you downloaded (bridge-cluster-k8s.yaml). It’s one file
containing all the resources. Update these pieces:
The Secret - paste in the values from Step 2:
apiVersion: v1
kind: Secret
metadata:
name: bridge-secrets
namespace: bridge
stringData:
BRIDGE_DATASTORE_HOST: "bridge-postgres"
BRIDGE_DATASTORE_PORT: "5432"
BRIDGE_DATASTORE_USER: "bridge"
BRIDGE_DATASTORE_PASSWORD: "bridge"
BRIDGE_DATASTORE_NAME: "bridge"
BRIDGE_DATASTORE_SSLMODE: "disable"
BRIDGE_ENCRYPTION_KEY_B64: "<ENCRYPTION_KEY_B64 from Step 2>"
BRIDGE_CLUSTER_TOKEN: "<CLUSTER_TOKEN from Step 2>"
BRIDGE_HOST_KEY_SEED: "<HOST_KEY_SEED from Step 2>"The ConfigMap - this holds the Bridge configuration. Make sure the cluster
settings use the current option names shown here, and that the orchestrator’s
provisioner uses the public Bridge image (or your private-registry mirror):
apiVersion: v1
kind: ConfigMap
metadata:
name: bridge-config
namespace: bridge
data:
config.yaml: |
cluster:
session:
max_concurrent_sessions: 1
session_limit: 1 # one session, then the worker is replaced
lifespan: "0s" # no time-based replacement
proxy:
lifespan: "0s" # "0s" = don't cycle proxies; e.g. "36h" in prod
drain_deadline: "30s"
max_cycle_concurrency: 1
orchestrator:
warm_session_workers: 3 # keep 3 session pods ready
proxy_workers: 1 # keep 1 proxy pod running
reconcile: "5s" # how often to check and top up
provisioner:
platform: "k8s"
path: "/opt/bridge/drivers/k8s/driver"
k8s:
image: "britive/bridge:latest"
namespace: "bridge"
server:
listen: "8080"
tls:
enabled: false # terminate TLS at an Ingress/load balancer
recording:
output_dir: /data/recordings
auth:
types: ["britive"]
britive:
tenant: "your-tenant" # subdomain only, e.g. "acme" — see /reference/finding-your-tenant
ssh:
native: { enabled: true, listen: "2222" }
browser: { enabled: true }
idle_timeout: "30m"Use the current option names. Older copies of this manifest may use names like
session_target, proxy_target, max_sessions, lifespan_minutes, or
driver_path. Those are no longer read and will be silently ignored (falling back
to defaults). The correct names are shown above: warm_session_workers,
proxy_workers, session_limit, lifespan (a duration string), reconcile, and
the provisioner block with platform/path/k8s.image/k8s.namespace.
The image references - the manifest already uses britive/bridge:latest
everywhere. Only change the image: fields (orchestrator Deployment and the
provisioner.k8s.image) if you mirrored the image to a private registry in
Step 1.
Recordings storage. The starting manifest uses a node-local path for
recordings, which only works on a single node. For a multi-node cluster, replace
it with a ReadWriteMany volume (EFS, Azure Files, NFS, and similar) mounted at
/data/recordings on the worker pods, so any pod can read any recording.
Apply It
kubectl apply -f bridge-cluster-k8s.yamlThis creates the namespace, database, config, secret, RBAC, orchestrator, and the proxy Service.
Verify
Watch the orchestrator start
It runs migrations and then begins creating workers:
kubectl logs -f -n bridge deployment/bridge-orchestratorWatch the session and proxy pods appear
kubectl get pods -n bridge -L bridge.roleAfter a moment you should see bridge-orchestrator, one or more proxy pods, and
several session pods (matching warm_session_workers).
Reach the web app
Find the proxy Service’s external address:
kubectl get svc -n bridge bridge-proxyIf your cluster provisions load balancers, use the external IP. Otherwise, port-forward for a quick test:
kubectl port-forward -n bridge svc/bridge-proxy 8080:8888
# then open http://localhost:8080/Production Notes
- TLS: Put an Ingress or cloud load balancer with a real certificate in
front of the proxy Service, and keep
tls.enabled: falsein the config so TLS is terminated at the edge. - Database: Use a managed PostgreSQL service instead of the in-cluster
PostgreSQL pod, and point the Secret’s
BRIDGE_DATASTORE_*values at it (setBRIDGE_DATASTORE_SSLMODEtorequire). - Recordings: Use a ReadWriteMany persistent volume so recordings survive pod
replacement and are readable cluster-wide. The volume must be writable by uid
102and gid103. For EFS, use an access point that owns the root directory as102:103; the EFS CSI driver does not applyfsGroupownership. - Resources: Add CPU/memory requests and limits to the pods, sized for your session load (session pods do the heavy work).
- Image pulls: Pin a tested Bridge image tag for production. If your cluster cannot pull from Docker Hub, mirror that same tag into a private registry and update both image references in the manifest.
Troubleshoot
| Symptom | Likely Cause | Fix |
|---|---|---|
| Orchestrator pod crash-loops | Can’t reach PostgreSQL, or config invalid | kubectl logs -n bridge deployment/bridge-orchestrator; check the Secret’s datastore values and the ConfigMap YAML. |
| No session/proxy pods appear | Orchestrator lacks permission to create pods, or the driver image/namespace is wrong | Confirm the ServiceAccount + Role/RoleBinding are applied; check the provisioner.k8s.image and namespace. |
| Settings seem ignored (wrong worker count) | ConfigMap uses old option names | Update to the current names (see the warning in Step 3). |
| Recordings missing after a pod is replaced | Node-local storage on a multi-node cluster | Switch to a ReadWriteMany shared volume. |
| Can’t reach the web app | No load balancer controller, or wrong Service type | Use port-forward to test, or add an Ingress/LoadBalancer appropriate to your platform. |
Next Steps
- Configure authentication
- Install and manage your license.
- Review protocols, recording, operations for the Admin console, upgrades, and backups.