Skip to main content

Overview

Deploy the MCP Server with LangGraph on Kubernetes for production-grade reliability, scalability, and high availability. This guide covers deployment with Keycloak SSO, Redis sessions, OpenFGA authorization, and observability.
v2.4.0 includes updated infrastructure (OpenFGA v1.10.2, Keycloak 26.4.0, PostgreSQL 16), production-ready Kubernetes manifests with Keycloak, Redis, and comprehensive security configurations.

Architecture

Prerequisites

1

Kubernetes Cluster

Minimum requirements:
  • Kubernetes 1.25+
  • 4 vCPUs, 8GB RAM
  • 50GB storage
  • kubectl configured
Recommended:
  • 8+ vCPUs, 16GB+ RAM
  • 100GB+ SSD storage
  • Multi-zone cluster
  • Auto-scaling enabled
2

Container Registry

Build and push the Docker image:
# Build image
docker build -t gcr.io/your-project/mcp-server-langgraph:v2.4.0 .

# Push to registry
docker push gcr.io/your-project/mcp-server-langgraph:v2.4.0
Update deployments/kubernetes/base/deployment.yaml:
spec:
  template:
    spec:
      containers:
      - name: mcp-server-langgraph
        image: gcr.io/your-project/mcp-server-langgraph:v2.4.0
3

Install Tools

# kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl && sudo mv kubectl /usr/local/bin/

# kustomize (optional)
curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
sudo mv kustomize /usr/local/bin/

# helm (for dependencies)
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

Quick Deploy

## 1. Create namespace
kubectl apply -f deployments/kubernetes/base/namespace.yaml

## 2. Create secrets
kubectl create secret generic mcp-server-langgraph-secrets \
  --namespace=mcp-server-langgraph \
  --from-literal=anthropic-api-key="${ANTHROPIC_API_KEY}" \
  --from-literal=google-api-key="${GOOGLE_API_KEY}" \
  --from-literal=jwt-secret-key="$(openssl rand -base64 32)" \
  --from-literal=keycloak-client-secret="${KEYCLOAK_CLIENT_SECRET}" \
  --from-literal=redis-password="$(openssl rand -base64 32)" \
  --from-literal=openfga-store-id="${OPENFGA_STORE_ID}" \
  --from-literal=openfga-model-id="${OPENFGA_MODEL_ID}"

## 3. Deploy dependencies (Keycloak, Redis, OpenFGA)
kubectl apply -f deployments/kubernetes/base/keycloak-deployment.yaml
kubectl apply -f deployments/kubernetes/base/keycloak-service.yaml
kubectl apply -f deployments/kubernetes/base/redis-session-deployment.yaml
kubectl apply -f deployments/kubernetes/base/redis-session-service.yaml
## Note: OpenFGA deployment not shown - use Helm chart or manifest

## 4. Wait for dependencies
kubectl wait --for=condition=available --timeout=300s \
  deployment/keycloak -n mcp-server-langgraph
kubectl wait --for=condition=available --timeout=300s \
  deployment/redis-session -n mcp-server-langgraph

## 5. Deploy application
kubectl apply -f deployments/kubernetes/base/

## 6. Wait for rollout
kubectl rollout status deployment/mcp-server-langgraph -n mcp-server-langgraph

## 7. Verify
kubectl get pods -n mcp-server-langgraph
kubectl logs -l app=mcp-server-langgraph -n mcp-server-langgraph --tail=50

Step-by-Step Deployment

1. Namespace and RBAC

## Create namespace
kubectl apply -f - << 'EOF'
apiVersion: v1
kind: Namespace
metadata:
  name: mcp-server-langgraph
  labels:
    name: mcp-server-langgraph
    environment: production
EOF

## Create service account
kubectl apply -f deployments/kubernetes/base/serviceaccount.yaml

2. Configuration

ConfigMap for application configuration:
kubectl apply -f deployments/kubernetes/base/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mcp-server-langgraph-config
  namespace: mcp-server-langgraph
data:
  # Service
  environment: "production"
  log_level: "INFO"

  # LLM
  llm_provider: "anthropic"
  model_name: "claude-sonnet-4-5-20250929"
  enable_fallback: "true"

  # Authentication
  auth_provider: "keycloak"
  auth_mode: "session"

  # Keycloak
  keycloak_server_url: "http://keycloak:8080"
  keycloak_realm: "mcp-server-langgraph"
  keycloak_client_id: "langgraph-client"

  # Redis Sessions
  session_backend: "redis"
  redis_url: "redis://redis-session:6379/0"
  session_ttl_seconds: "86400"

  # OpenFGA
  openfga_api_url: "http://openfga:8080"
Secrets for sensitive data:
## Generate secrets
export JWT_SECRET=$(openssl rand -base64 32)
export REDIS_PASSWORD=$(openssl rand -base64 32)

## Create secret
kubectl create secret generic mcp-server-langgraph-secrets \
  --namespace=mcp-server-langgraph \
  --from-literal=anthropic-api-key="${ANTHROPIC_API_KEY}" \
  --from-literal=google-api-key="${GOOGLE_API_KEY}" \
  --from-literal=openai-api-key="${OPENAI_API_KEY}" \
  --from-literal=jwt-secret-key="${JWT_SECRET}" \
  --from-literal=keycloak-client-secret="${KEYCLOAK_CLIENT_SECRET}" \
  --from-literal=redis-password="${REDIS_PASSWORD}" \
  --from-literal=openfga-store-id="${OPENFGA_STORE_ID}" \
  --from-literal=openfga-model-id="${OPENFGA_MODEL_ID}"
Never commit secrets to Git! Use external secret managers (see Infisical Setup) or cloud-native solutions (GCP Secret Manager, AWS Secrets Manager, Azure Key Vault).

3. Deploy Keycloak

## Deploy Keycloak with PostgreSQL
helm install keycloak bitnami/keycloak \
  --namespace mcp-server-langgraph \
  --set auth.adminUser=admin \
  --set auth.adminPassword="$(openssl rand -base64 32)" \
  --set postgresql.enabled=true \
  --set postgresql.auth.password="$(openssl rand -base64 32)" \
  --set replicaCount=2 \
  --set ingress.enabled=true \
  --set ingress.hostname=sso.yourdomain.com

## Wait for deployment
kubectl wait --for=condition=available --timeout=300s \
  deployment/keycloak -n mcp-server-langgraph

## Initialize Keycloak
python scripts/setup/setup_keycloak.py

## Save client secret to Kubernetes secret
kubectl patch secret mcp-server-langgraph-secrets \
  -n mcp-server-langgraph \
  --patch "{\"data\":{\"keycloak-client-secret\":\"$(echo -n $CLIENT_SECRET | base64)\"}}"
See Keycloak SSO Guide for detailed setup.

4. Deploy Redis

## Deploy Redis with replication
helm install redis-session bitnami/redis \
  --namespace mcp-server-langgraph \
  --set auth.password="${REDIS_PASSWORD}" \
  --set master.persistence.enabled=true \
  --set master.persistence.size=10Gi \
  --set replica.replicaCount=2 \
  --set replica.persistence.enabled=true \
  --set sentinel.enabled=true

## Wait for deployment
kubectl wait --for=condition=ready pod \
  -l app.kubernetes.io/name=redis \
  -n mcp-server-langgraph \
  --timeout=300s

## Test connection
kubectl run redis-test --rm -i --tty \
  --image redis:7-alpine \
  --namespace mcp-server-langgraph \
  -- redis-cli -h redis-session -a $REDIS_PASSWORD ping
## Expected: PONG
See Redis Sessions Guide for details.

5. Deploy OpenFGA

## Deploy OpenFGA with PostgreSQL
kubectl apply -f - << 'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: openfga
  namespace: mcp-server-langgraph
spec:
  replicas: 2
  selector:
    matchLabels:
      app: openfga
  template:
    metadata:
      labels:
        app: openfga
    spec:
      containers:
      - name: openfga
        image: openfga/openfga:v1.10.2
        args:
        - run
        env:
        - name: OPENFGA_DATASTORE_ENGINE
          value: postgres
        - name: OPENFGA_DATASTORE_URI
          value: "postgres://openfga:password@postgres:5432/openfga"
        ports:
        - containerPort: 8080
          name: http
        - containerPort: 8081
          name: grpc
---
apiVersion: v1
kind: Service
metadata:
  name: openfga
  namespace: mcp-server-langgraph
spec:
  selector:
    app: openfga
  ports:
  - name: http
    port: 8080
    targetPort: 8080
  - name: grpc
    port: 8081
    targetPort: 8081
EOF

## Initialize OpenFGA
python scripts/setup/setup_openfga.py

6. Deploy Application

## Deploy all manifests
kubectl apply -f deployments/kubernetes/base/deployment.yaml
kubectl apply -f deployments/kubernetes/base/service.yaml
kubectl apply -f deployments/kubernetes/base/hpa.yaml
kubectl apply -f deployments/kubernetes/base/pdb.yaml
kubectl apply -f deployments/kubernetes/base/ingress-http.yaml

## Wait for rollout
kubectl rollout status deployment/mcp-server-langgraph -n mcp-server-langgraph

## Check pods
kubectl get pods -n mcp-server-langgraph -l app=mcp-server-langgraph

7. Configure Ingress

  • NGINX Ingress
  • GKE Ingress
  • AWS ALB
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: mcp-server-langgraph
  namespace: mcp-server-langgraph
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/proxy-body-size: "10m"
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - api.yourdomain.com
    secretName: mcp-server-langgraph-tls
  rules:
  - host: api.yourdomain.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: mcp-server-langgraph
            port:
              number: 8000
kubectl apply -f ingress.yaml

High Availability

Horizontal Pod Autoscaling

## deployments/kubernetes/base/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: mcp-server-langgraph
  namespace: mcp-server-langgraph
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: mcp-server-langgraph
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
      - type: Percent
        value: 50
        periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
      - type: Percent
        value: 100
        periodSeconds: 60

Pod Disruption Budget

## deployments/kubernetes/base/pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: mcp-server-langgraph
  namespace: mcp-server-langgraph
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: mcp-server-langgraph

Pod Anti-Affinity

Spread pods across availability zones:
affinity:
  podAntiAffinity:
    preferredDuringSchedulingIgnoredDuringExecution:
    - weight: 100
      podAffinityTerm:
        labelSelector:
          matchExpressions:
          - key: app
            operator: In
            values:
            - mcp-server-langgraph
        topologyKey: topology.kubernetes.io/zone

Security

Network Policies

## deployments/kubernetes/base/networkpolicy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: mcp-server-langgraph
  namespace: mcp-server-langgraph
spec:
  podSelector:
    matchLabels:
      app: mcp-server-langgraph
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          name: ingress-nginx
    ports:
    - protocol: TCP
      port: 8000
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: keycloak
    ports:
    - protocol: TCP
      port: 8080
  - to:
    - podSelector:
        matchLabels:
          app: redis-session
    ports:
    - protocol: TCP
      port: 6379
  - to:
    - podSelector:
        matchLabels:
          app: openfga
    ports:
    - protocol: TCP
      port: 8080
  - to:
    - namespaceSelector: {}
    ports:
    - protocol: TCP
      port: 443  # HTTPS for LLM APIs

Pod Security Standards

apiVersion: v1
kind: Namespace
metadata:
  name: mcp-server-langgraph
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted

Security Context

securityContext:
  runAsNonRoot: true
  runAsUser: 1000
  fsGroup: 1000
  seccompProfile:
    type: RuntimeDefault
  capabilities:
    drop:
    - ALL
  readOnlyRootFilesystem: true
  allowPrivilegeEscalation: false

Observability

Prometheus Metrics

## Pod annotations for Prometheus scraping
annotations:
  prometheus.io/scrape: "true"
  prometheus.io/port: "8000"
  prometheus.io/path: "/metrics/prometheus"
Deploy Prometheus:
helm install prometheus prometheus-community/kube-prometheus-stack \
  --namespace observability --create-namespace

OpenTelemetry

Deploy OpenTelemetry Collector:
helm install opentelemetry-collector open-telemetry/opentelemetry-collector \
  --namespace observability \
  --set mode=deployment \
  --set config.exporters.otlp.endpoint=jaeger:4317

Jaeger Tracing

helm install jaeger jaegertracing/jaeger \
  --namespace observability \
  --set provisionDataStore.cassandra=false \
  --set allInOne.enabled=true \
  --set storage.type=memory
Access Jaeger UI:
kubectl port-forward svc/jaeger-query 16686:16686 -n observability
## Open: http://localhost:16686

Health Checks

The deployment includes comprehensive health checks:
startupProbe:
  httpGet:
    path: /health/startup
    port: 8000
  initialDelaySeconds: 10
  periodSeconds: 5
  failureThreshold: 30

livenessProbe:
  httpGet:
    path: /health
    port: 8000
  periodSeconds: 10
  failureThreshold: 3

readinessProbe:
  httpGet:
    path: /health/ready
    port: 8000
  periodSeconds: 5
  failureThreshold: 3
Check health:
kubectl exec -it deploy/mcp-server-langgraph -n mcp-server-langgraph -- \
  curl http://localhost:8000/health

Troubleshooting

# Check pod status
kubectl get pods -n mcp-server-langgraph

# Describe pod
kubectl describe pod POD_NAME -n mcp-server-langgraph

# Check logs
kubectl logs POD_NAME -n mcp-server-langgraph

# Check events
kubectl get events -n mcp-server-langgraph --sort-by='.lastTimestamp'
# Check init container logs
kubectl logs POD_NAME -n mcp-server-langgraph -c wait-for-keycloak
kubectl logs POD_NAME -n mcp-server-langgraph -c wait-for-redis
kubectl logs POD_NAME -n mcp-server-langgraph -c wait-for-openfga

# Test connectivity manually
kubectl run test --rm -it --image=busybox -- sh
nc -zv keycloak 8080
nc -zv redis-session 6379
nc -zv openfga 8080
# Check readiness
kubectl get pods -n mcp-server-langgraph -o wide

# Test health endpoint
kubectl exec -it deploy/mcp-server-langgraph -n mcp-server-langgraph -- \
  curl -v http://localhost:8000/health

# Check dependencies
kubectl logs deploy/mcp-server-langgraph -n mcp-server-langgraph | grep -i error
# Check resource usage
kubectl top pods -n mcp-server-langgraph

# View metrics
kubectl get --raw /apis/metrics.k8s.io/v1beta1/namespaces/mcp-server-langgraph/pods

# Adjust limits in deployment.yaml
resources:
  limits:
    memory: 4Gi  # Increase limit

Next Steps


Production Ready: Your MCP Server is now running on Kubernetes with high availability and enterprise-grade security!