Introduction
Production MCP servers require reproducible, version-controlled infrastructure. Infrastructure as Code (IaC) ensures that your development, staging, and production environments are consistent, that deployments are repeatable, and that infrastructure changes go through the same review process as application code. This lesson covers Docker Compose for local development, Kubernetes for production, and configuration management patterns.
Key Concepts
- Docker Compose: A tool for defining multi-container applications. Ideal for local development where you need multiple MCP servers, databases, and supporting services running together.
- Kubernetes (K8s): A container orchestration platform for deploying, scaling, and managing containerized MCP servers in production.
- Health Check Endpoints: HTTP endpoints that orchestrators probe to determine if a server is ready to accept traffic.
- Environment-Based Configuration: Using environment variables and configuration files to vary behavior across dev, staging, and production without code changes.
- Secrets Management: Securely storing and injecting API keys, database credentials, and OAuth client secrets into server processes.
Real World Context
A platform engineering team manages 8 MCP servers. Without IaC, deploying a new server involves manual Docker commands, ad-hoc environment variable configuration, and undocumented networking rules. When an engineer is on vacation, deployments stall. With IaC, any team member can deploy by running kubectl apply or docker compose up, and all configuration is versioned in Git.
Deep Dive
Docker Compose for Local Development
Define all MCP servers and dependencies in a single file:
yamlversion: "3.8" services: # Supporting infrastructure postgres: image: postgres:16-alpine environment: POSTGRES_DB: mcp_dev POSTGRES_USER: mcp POSTGRES_PASSWORD: devpassword ports: - "5432:5432" volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U mcp"] interval: 10s timeout: 5s retries: 5 redis: image: redis:7-alpine ports: - "6379:6379" healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s # MCP Servers db-tools: build: context: ./servers/db-tools dockerfile: Dockerfile environment: - DATABASE_URL=postgresql://mcp:devpassword@postgres:5432/mcp_dev - REDIS_URL=redis://redis:6379 - NODE_ENV=development - LOG_LEVEL=debug ports: - "3101:3100" depends_on: postgres: condition: service_healthy redis: condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3100/health"] interval: 30s file-tools: build: ./servers/file-tools environment: - ALLOWED_ROOT=/data - NODE_ENV=development volumes: - ./test-data:/data:ro ports: - "3102:3100" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3100/health"] interval: 30s volumes: pgdata:
This composition spins up PostgreSQL, Redis, and two MCP servers with proper health check dependencies. The depends_on with condition: service_healthy ensures MCP servers only start after their dependencies are ready.
Kubernetes Deployment
For production, deploy MCP servers as Kubernetes Deployments with associated Services:
yamlapiVersion: apps/v1 kind: Deployment metadata: name: mcp-db-tools labels: app: mcp-db-tools spec: replicas: 3 selector: matchLabels: app: mcp-db-tools template: metadata: labels: app: mcp-db-tools spec: containers: - name: mcp-server image: registry.example.com/mcp-db-tools:v1.2.0 ports: - containerPort: 3100 env: - name: DATABASE_URL valueFrom: secretKeyRef: name: mcp-db-secrets key: database-url - name: NODE_ENV value: "production" resources: requests: memory: "128Mi" cpu: "100m" limits: memory: "512Mi" cpu: "500m" readinessProbe: httpGet: path: /health port: 3100 initialDelaySeconds: 10 periodSeconds: 15 livenessProbe: httpGet: path: /health port: 3100 initialDelaySeconds: 30 periodSeconds: 30 securityContext: runAsNonRoot: true readOnlyRootFilesystem: true allowPrivilegeEscalation: false --- apiVersion: v1 kind: Service metadata: name: mcp-db-tools spec: selector: app: mcp-db-tools ports: - port: 80 targetPort: 3100 type: ClusterIP
The deployment runs 3 replicas with resource limits, readiness and liveness probes, and a security context that enforces non-root execution and read-only filesystems.
Secrets Management
Never hardcode secrets. Use Kubernetes Secrets or external vaults:
yamlapiVersion: v1 kind: Secret metadata: name: mcp-db-secrets type: Opaque data: database-url: cG9zdGdyZXNxbDovL21jcDpzZWN1cmVwYXNzQGRiOjU0MzIvbWNw # base64 oauth-client-secret: c2VjcmV0MTIz # base64
For more robust secrets management, integrate with HashiCorp Vault, AWS Secrets Manager, or GCP Secret Manager using init containers or sidecar injectors.
Environment-Based Configuration
Use a configuration pattern that adapts to the deployment environment:
typescriptinterface ServerConfig { port: number; logLevel: string; rateLimitRpm: number; dbPoolSize: number; corsOrigins: string[]; } function loadConfig(): ServerConfig { const env = process.env.NODE_ENV ?? "development"; const configs: Record<string, ServerConfig> = { development: { port: 3100, logLevel: "debug", rateLimitRpm: 1000, dbPoolSize: 5, corsOrigins: ["http://localhost:*"] }, staging: { port: 3100, logLevel: "info", rateLimitRpm: 500, dbPoolSize: 10, corsOrigins: ["https://staging.example.com"] }, production: { port: 3100, logLevel: "warn", rateLimitRpm: 200, dbPoolSize: 20, corsOrigins: ["https://mcp.example.com"] } }; return configs[env] ?? configs.development; }
This centralizes configuration and makes environment differences explicit and reviewable.
Common Pitfalls
- Hardcoded secrets in Docker images: Never bake secrets into Docker images or Dockerfiles. Use environment variables or mounted secrets at runtime.
- Missing readiness probes: Without readiness probes, Kubernetes routes traffic to pods that are still initializing, causing errors for the first few requests.
- No resource limits: Without CPU and memory limits, a single runaway pod can consume all node resources, causing cascading failures.
Best Practices
- Version your infrastructure alongside your code: Keep Dockerfiles, Compose files, and Kubernetes manifests in the same repository as the server code.
- Use readiness AND liveness probes: Readiness controls traffic routing; liveness triggers automatic restarts. They serve different purposes and should have different thresholds.
- Pin image versions: Use specific tags (e.g.,
v1.2.0) rather thanlatestto ensure reproducible deployments and safe rollbacks.
Summary
- Docker Compose defines multi-server local development environments with health check dependencies
- Kubernetes Deployments provide production-grade scaling, rolling updates, and self-healing
- Health check endpoints (readiness + liveness) are essential for orchestrated deployments
- Secrets must never be hardcoded—use Kubernetes Secrets, Vault, or cloud secret managers
- Environment-based configuration adapts behavior across dev, staging, and production without code changes
Code Examples
apiVersion: apps/v1
kind: Deployment
metadata:
name: mcp-server
spec:
replicas: 3
selector:
matchLabels:
app: mcp-server
template:
spec:
containers:
- name: server
image: registry.example.com/mcp-server:v1.0.0
ports:
- containerPort: 3100
readinessProbe:
httpGet:
path: /health
port: 3100
initialDelaySeconds: 10
livenessProbe:
httpGet:
path: /health
port: 3100
initialDelaySeconds: 30
securityContext:
runAsNonRoot: true
readOnlyRootFilesystem: true