{"slug": "deploying-mcp-servers-on-kubernetes", "title": "Deploying MCP Servers on Kubernetes", "summary": "A developer detailed how to deploy Model Context Protocol (MCP) servers on Kubernetes, arguing the key change is switching the server's transport from stdio to Streamable HTTP before containerization, since a stdio process inside a container has no listener for a Service to route to. The writeup covers separate liveness and readiness probes, a 45-second termination grace period, and credential handling via IRSA or Workload Identity, noting the July 2026 MCP spec removed protocol-level sessions so no session affinity is needed.", "body_md": "Most MCP servers start life as a local, stdio-based process — the client spawns the server as a child process, messages flow over stdin and stdout. The instinct once that server needs to serve more than one client is to containerize it and expose a port. That instinct produces a container that starts, passes a naive health check, and then does nothing useful.\n\nA process reading from stdin and writing to stdout inside a container has no listener, no port, nothing for a Kubernetes Service to route traffic toward. The fix isn't a networking change — it's a transport change in the server itself, from stdio to Streamable HTTP, before Kubernetes enters the picture at all.\n\nThe Dockerfile is unremarkable by design — the interesting decisions happen in the manifests, not the image. What matters is that the process binds to HTTP transport, not stdio, as its entrypoint:\n\n```\nFROM node:22-slim AS builder\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci --omit=dev\nCOPY . .\nRUN npm run build\n\nFROM node:22-slim\nWORKDIR /app\nCOPY --from=builder /app/dist ./dist\nCOPY --from=builder /app/node_modules ./node_modules\nENV MCP_TRANSPORT=streamable-http\nENV MCP_HTTP_ADDR=0.0.0.0:8080\nENV MCP_HTTP_ENDPOINT=/mcp\nEXPOSE 8080\nUSER node\nENTRYPOINT [\"node\", \"dist/server.js\"]\n```\n\nThe three `MCP_*` variables mirror the flags production-grade MCP server implementations expose for choosing transport mode at startup. Transport, bind address, and endpoint path all need to be explicit and set at deploy time, not hardcoded into the binary.\n\nAn MCP server mid-tool-call is not the same thing as a hung process. The probes need to say so explicitly:\n\n```\nlivenessProbe:\n  httpGet:\n    path: /health          # answers: is the process itself alive?\n    port: 8080\n  periodSeconds: 15\n  timeoutSeconds: 5\n  failureThreshold: 3      # ~45s of true unresponsiveness before restart\n\nreadinessProbe:\n  httpGet:\n    path: /ready            # answers: can this pod accept new work right now?\n    port: 8080\n  periodSeconds: 5\n  failureThreshold: 2\n```\n\n`/health` should do nothing more than confirm the process's event loop is running — never block on the status of an in-progress tool call. `/ready` reflects capacity: a pod mid-reasoning can report itself not-ready for new work without being treated as dead.\n\nTwo other details in the Deployment matter: `terminationGracePeriodSeconds: 45` (the default 30s is a poor fit for a process that might be mid-tool-call during a rolling update), and resource requests set deliberately modest relative to limits — MCP servers spend most of their wall-clock time waiting on outbound I/O, not computing locally.\n\n```\napiVersion: v1\nkind: Service\nmetadata:\n  name: mcp-server\nspec:\n  selector:\n    app: mcp-server\n  ports:\n    - port: 443\n      targetPort: 8080\n      name: https\n```\n\nNo session affinity. No cookie-based stickiness annotation. Nothing beyond ordinary load balancing. This is the direct payoff of the July 2026 MCP spec removing the protocol-level session — any request can land on any pod, so the Service stays genuinely simple.\n\nAn MCP server calling AWS APIs needs credentials. The deployment-time decision that matters most is avoiding a long-lived static credential baked into the image or mounted as a Secret. IAM Roles for Service Accounts (IRSA) binds a Kubernetes ServiceAccount to an IAM role via OIDC federation:\n\n```\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: mcp-server\n  annotations:\n    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/mcp-server-role\n```\n\nThe IAM role should be scoped narrowly to whatever the server's tools actually need — an MCP server's tool surface is exactly the kind of thing that grows without anyone revisiting the IAM policy that backs it.\n\nWorkload Identity is AKS's equivalent — it federates a Kubernetes ServiceAccount token with an Entra ID application:\n\n```\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: mcp-server\n  annotations:\n    azure.workload.identity/client-id: \"11111111-2222-3333-4444-555555555555\"\n  labels:\n    azure.workload.identity/use: \"true\"\n```\n\nThe `azure.workload.identity/use: \"true\"` label must be present on both the ServiceAccount and the pod template — a pod missing that label silently falls back to no identity at all rather than failing loudly, which makes it a frustrating thing to debug after the fact.\n\nCPU utilization doesn't track I/O-bound tool-call concurrency well, but a deployment needs something running from day one:\n\n```\nspec:\n  minReplicas: 3\n  maxReplicas: 10\n  metrics:\n    - type: Resource\n      resource:\n        name: cpu\n        target:\n          type: Utilization\n          averageUtilization: 70\n  behavior:\n    scaleUp:\n      stabilizationWindowSeconds: 30\n    scaleDown:\n      stabilizationWindowSeconds: 300\n```\n\nThe asymmetric stabilization windows are deliberate: scale up quickly (30s) to protect against bursts of concurrent tool-calling tasks, scale down slowly (300s) to avoid flapping. This will under-react to the specific load pattern MCP servers produce, but it's a safe default while a better signal is put in place.\n\n```\nstrategy:\n  type: RollingUpdate\n  rollingUpdate:\n    maxUnavailable: 0\n    maxSurge: 1\n```\n\n`maxUnavailable: 0` guarantees the rollout never reduces capacity below the current replica count — a new pod must pass its readiness probe before the corresponding old pod receives SIGTERM and begins the 45-second grace period. The difference between a rollout that's invisible to callers and one that produces a visible error-rate blip every time code ships.\n\nA request that exercises the actual transport confirms the deployment is correctly speaking the current specification:\n\n```\ncurl -s -X POST https://mcp.example.com/mcp \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Mcp-Method: tools/list\" \\\n  -H \"Mcp-Name: mcp-server\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\",\"params\":{}}'\n```\n\nA correctly deployed 2026-07-28-compliant server returns the tool list with no `Mcp-Session-Id` in the response headers and no `initialize` call required beforehand. If a session header shows up, the server (or a proxy in front of it) is still running the older stateful transport path. Worth catching in verification rather than discovering under load — and worth running as a smoke test on every deploy, not just at initial deployment.\n\nThe summary covers the core patterns. The full article goes deeper on:\n\n`maxUnavailable: 0` is the right default for this workload class", "url": "https://wpnews.pro/news/deploying-mcp-servers-on-kubernetes", "canonical_source": "https://dev.to/aloknecessary/deploying-mcp-servers-on-kubernetes-3435", "published_at": "2026-10-16 06:30:46+00:00", "updated_at": "2026-10-16 06:35:16.193218+00:00", "lang": "en", "topics": ["ai-agents", "agent-protocols", "ai-infrastructure", "mlops", "developer-tools"], "entities": ["Model Context Protocol", "Kubernetes", "AWS", "IAM Roles for Service Accounts", "Azure Kubernetes Service", "Entra ID", "Docker"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/deploying-mcp-servers-on-kubernetes", "markdown": "https://wpnews.pro/news/deploying-mcp-servers-on-kubernetes.md", "text": "https://wpnews.pro/news/deploying-mcp-servers-on-kubernetes.txt", "jsonld": "https://wpnews.pro/news/deploying-mcp-servers-on-kubernetes.jsonld"}}