Skip to content
Kubernetes Deployment

Kubernetes Deployment

This guide deploys the Gateway on Kubernetes as an ordinary stateless Deployment. It works on managed Kubernetes (EKS, GKE, AKS) and on self-managed clusters.

There is nothing unusual to model: no StatefulSet, no persistent volume, no leader election. All state lives in Postgres, so the Gateway is a plain horizontally-scaled web workload.

Overview

You’ll create these in a dedicated britive-mcp-gateway namespace:

  • A Secret holding the tenant, pool token, and database URL. That is the whole of the pod’s configuration; every other setting comes from the gateway pool on your Britive tenant.
  • A Deployment of two Gateway replicas, with startup, readiness, and liveness probes.
  • A Service exposing them inside the cluster.
  • An Ingress as one way to get traffic in.

Architecture

    flowchart LR
  Clients["MCP clients and agents"]
  Britive["Britive tenant"]
  Backends["Backend MCP servers"]

  subgraph ns["Namespace: britive-mcp-gateway"]
    Ing["Ingress<br/>TLS termination"]
    Svc["Service<br/>ClusterIP"]
    P1["Gateway pod"]
    P2["Gateway pod"]
    Ing --> Svc
    Svc --> P1
    Svc --> P2
  end

  DB[("Postgres<br/>(yours)")]

  Clients -->|"HTTPS"| Ing
  P1 --> DB
  P2 --> DB
  P1 --> Britive
  P1 --> Backends
  

Before You Begin

You need:

  • A Kubernetes cluster and kubectl configured for it.
  • A Postgres 14+ database the pods can reach. Not created by this manifest - a managed service is the expected choice.
  • An ingress controller or another way to expose a Service, plus a TLS certificate.
  • A Britive tenant and pool token.

Deploy

Download the manifest

Download mcp-gateway-k8s.yaml - Namespace, Secret, Deployment, Service, and Ingress.

Fill in the Secret

Replace the three REPLACE_ME values in the manifest’s Secret: TENANT, GATEWAY_POOL_TOKEN, and DATABASE_URL.

That is the whole of the pod’s configuration. There is no master secret to generate and no ConfigMap of settings: the key protecting stored tokens and sessions is minted by the platform for the gateway pool and arrives with the pool’s settings, so every pod agrees on it without anything being distributed.

For a real cluster, prefer the External Secrets Operator, Sealed Secrets, or your cloud’s secret store over committing a manifest with values in it.

Set the public URL on the pool

In the Britive tenant portal, open the gateway pool your token belongs to and set Public base URL to the hostname clients will use - the same hostname as the Ingress:

https://mcp-gateway.example.internal

OAuth redirect and resource URLs are built from it, so it must match what the browser actually reaches - not the pod or the Service.

Apply it

kubectl apply -f mcp-gateway-k8s.yaml
kubectl -n britive-mcp-gateway rollout status deploy/mcp-gateway

Confirm the pods are healthy

kubectl -n britive-mcp-gateway get pods
kubectl -n britive-mcp-gateway logs deploy/mcp-gateway --tail=50

Both pods should be Running and ready. The Gateway names configuration problems explicitly in its startup logs.

Verify

kubectl -n britive-mcp-gateway port-forward svc/mcp-gateway 8080:80
curl -s http://localhost:8080/healthz

Then through the Ingress:

  • GET /healthz returns 200 at your hostname.
  • Signing in at the root URL reaches the admin console.
  • Deleting one pod leaves you signed in, and the replacement serves traffic - proof the pods share state through Postgres rather than holding it locally.
kubectl -n britive-mcp-gateway delete pod <one-pod-name>

Ingress Settings That Matter

The manifest sets three annotations because the defaults break real usage. They are written for nginx-ingress; use your controller’s equivalents.

mcp-gateway-k8s.yaml
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
nginx.ingress.kubernetes.io/proxy-buffering: "off"

The timeouts. A Britive checkout that needs human approval can take longer than a default 60 seconds. An ingress that cuts the connection at 60s fails those tool calls, and it looks like a Gateway problem.

Buffering off. MCP holds a stream open for server-to-client messages. Buffering it makes a working call look like a hang.

No session affinity is needed anywhere. Every pod shares one database, so any pod can serve any request - do not add sticky sessions to work around a problem, because it will not be the cause.

Scaling and Upgrades

kubectl -n britive-mcp-gateway scale deploy/mcp-gateway --replicas=4

The Deployment uses maxUnavailable: 0, so a rolling upgrade brings up a new pod before removing an old one and the service stays up:

kubectl -n britive-mcp-gateway set image deploy/mcp-gateway \
  gateway=britive/mcp-gateway:<tag>
kubectl -n britive-mcp-gateway rollout status deploy/mcp-gateway

Schema migrations run on startup, so there is no migration Job to run. The startupProbe gives that first run room to finish before the liveness probe starts counting failures.

Pin a specific image tag rather than latest, so a pod rescheduling does not silently change versions.

Changing Settings

Nothing beyond those three Secret keys is configured in the cluster. Audit sinks, rate limits, inspection, backend policy - all of it lives on the gateway pool in the Britive tenant portal, and every pod picks a change up on its next sync, within five minutes by default. No ConfigMap, no rollout, no restart.

That is deliberate: the person who changes a setting is a platform administrator, and the person who can kubectl rollout restart is frequently someone else. See Configuration.

Troubleshoot

SymptomCauseFix
Pods in CrashLoopBackOffMissing DATABASE_URL, unreachable database, or rejected pool tokenkubectl logs - the message names it.
Pods never become readyReadiness probe failing, or the database is slow to accept connectionsCheck /readyz and database connectivity from the pod.
CreateContainerConfigErrorSecret key missingConfirm all three keys exist in the Secret.
Pods exit naming the tenantThe tenant is unreachable from the pod, or the pool token was rejectedFetching settings is fatal at startup by design. The log names both causes; check egress and NetworkPolicy first.
OAuth fails at the callbackThe pool’s Public base URL doesn’t match the Ingress hostMake them agree.
Calls appear to hangIngress buffering the MCP streamSet proxy-buffering: "off".
Approval-gated calls time outIngress read timeout too lowRaise proxy-read-timeout.
Users signed out at randomPods reading different Secrets, or pool tokens from different poolsEvery pod must read the same Secret, and its token must belong to one pool.
A settings change hasn’t taken effectThe next sync hasn’t run, or the last one failedThe console’s Settings tab shows what each pod is running and flags stale settings.

Next Steps

Last updated on