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:

yaml
version: "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:

yaml
apiVersion: 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:

yaml
apiVersion: 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:

typescript
interface 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

  1. Hardcoded secrets in Docker images: Never bake secrets into Docker images or Dockerfiles. Use environment variables or mounted secrets at runtime.
  2. Missing readiness probes: Without readiness probes, Kubernetes routes traffic to pods that are still initializing, causing errors for the first few requests.
  3. No resource limits: Without CPU and memory limits, a single runaway pod can consume all node resources, causing cascading failures.

Best Practices

  1. Version your infrastructure alongside your code: Keep Dockerfiles, Compose files, and Kubernetes manifests in the same repository as the server code.
  2. Use readiness AND liveness probes: Readiness controls traffic routing; liveness triggers automatic restarts. They serve different purposes and should have different thresholds.
  3. Pin image versions: Use specific tags (e.g., v1.2.0) rather than latest to 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

yaml
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
✓ Completed