Back to all articles
MCP in Production: Registries, Docker, and Enterprise Patterns

MCP in Production: Registries, Docker, and Enterprise Patterns

Complete guide to deploying MCP servers in production. Covers official registries, containerization with Docker, OAuth 2.1 authentication, monitoring,...

Human-architected research synthesized with the assistance of AI personas.
8 min read

TL;DR / Executive Summary

Complete guide to deploying MCP servers in production. Covers official registries, containerization with Docker, OAuth 2.1 authentication, monitoring,...

💡 TL;DR (Too Long; Didn't Read)

Production MCP deployment requires: (1) Trusted registries - Official MCP Registry for discovery, Docker MCP Catalog for signed images, GitHub Registry for evaluation; (2) Secure containerization with Docker Gateway or Kubernetes with network policies, resource limits, and probes; (3) OAuth 2.1 authentication for clients, service accounts for upstream; (4) Monitoring with metrics, structured logs, and anomaly detection; (5) Multi-server orchestration with namespacing and collision detection.

The previous articles covered MCP fundamentals, implementation, and security. This one bridges the gap between "works on my machine" and "runs reliably in production." We will explore the registry ecosystem, containerization strategies, authentication patterns, and operational practices that separate hobby projects from enterprise deployments.


The Registry Landscape

MCP servers are scattered across GitHub repositories, npm packages, PyPI modules, and Docker images. The registry ecosystem that emerged in 2025 changes this.

Official MCP Registry

Launched in September 2025, the Model Context Protocol team released the official MCP Registry at registry.modelcontextprotocol.io. This is the canonical source of truth for publicly available MCP servers.

The registry provides:

  • Standardized discovery: REST API (GET /v0/servers) that clients can query programmatically
  • Metadata consistency: Each server publishes a standardized server.json manifest
  • Community moderation: Deny-listing for malicious servers and quality signals
  • Federated architecture: Downstream registries can mirror and extend the official registry

Docker MCP Catalog

The Docker MCP Catalog takes a different approach: instead of just listing servers, it provides containerized, signed images ready for deployment.

bash
docker mcp gateway run \ --verify-signatures \ --block-network \ --log-calls \ --cpus 1 \ --memory 2Gb

When you install from the Docker catalog, you get:

  • Cryptographic signatures verifying the image hasn't been tampered with
  • SBOMs documenting every component
  • Provenance attestations linking the image to source code
  • Continuous vulnerability scanning

GitHub MCP Registry

GitHub launched its MCP Registry in October 2025, directly integrated into the GitHub experience:

  • Repository README displayed prominently
  • One-click installation for VS Code
  • Star metrics and community activity
  • Direct links to issues, PRs, and contributors

Choosing Between Registries

RegistryBest ForKey Benefit
Official MCP RegistryCanonical discovery, API accessStandardized metadata, federated architecture
Docker MCP CatalogSecure deployment, local developmentSigned images, vulnerability scanning
GitHub MCP RegistryEvaluation, open-source serversTrust signals, community metrics

Containerization Patterns

Basic Docker Deployment

bash
docker run -d \ --name mcp-github \ -e GITHUB_TOKEN=$GITHUB_TOKEN \ mcp/github:latest

Security-Hardened Deployment

bash
docker mcp gateway run \ --verify-signatures \ --block-network \ --log-calls \ --cpus 1 \ --memory 2Gb
FlagFunction
--verify-signaturesOnly runs images with valid cryptographic signatures
--block-networkRestricts outbound network access
--log-callsLogs every tool invocation
--cpus 1 --memory 2GbResource limits

Kubernetes Deployment

yaml
apiVersion: apps/v1 kind: Deployment metadata: name: mcp-database-server spec: replicas: 3 selector: matchLabels: app: mcp-database-server template: metadata: labels: app: mcp-database-server spec: securityContext: runAsNonRoot: true readOnlyRootFilesystem: true containers: - name: mcp-server image: internal-registry.company.com/mcp/database:v1.2.0 resources: limits: cpu: "1" memory: "2Gi" requests: cpu: "500m" memory: "1Gi" env: - name: DATABASE_URL valueFrom: secretKeyRef: name: database-credentials key: url livenessProbe: httpGet: path: /health port: 8080 initialDelaySeconds: 10 periodSeconds: 30 readinessProbe: httpGet: path: /ready port: 8080 initialDelaySeconds: 5 periodSeconds: 10

Network Policies

yaml
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: mcp-server-policy spec: podSelector: matchLabels: app: mcp-database-server policyTypes: - Ingress - Egress ingress: - from: - podSelector: matchLabels: app: mcp-client ports: - protocol: TCP port: 8080 egress: - to: - podSelector: matchLabels: app: database ports: - protocol: TCP port: 5432

This policy ensures the MCP server only receives traffic from the MCP client and only makes outbound connections to the database.


Authentication Patterns

OAuth 2.1 for Client Authentication

The MCP specification recommends OAuth 2.1 with PKCE for client-to-server authentication:

  1. Client discovers OAuth endpoints via /.well-known/oauth-authorization-server
  2. Client generates PKCE code verifier and challenge
  3. Client redirects user to authorization endpoint
  4. User authenticates and grants permissions
  5. Server redirects back with authorization code
  6. Client exchanges code (plus PKCE verifier) for access tokens
  7. Client includes access token in MCP requests

API Keys for Simple Cases

typescript
app.use((req, res, next) => { const apiKey = req.headers["x-api-key"]; if (!apiKey || !validateApiKey(apiKey)) { return res.status(401).json({ error: "Invalid API key" }); } req.apiKeyInfo = getKeyInfo(apiKey); next(); });

API keys work but require careful management:

  • Rotate keys regularly
  • Use different keys per client/environment
  • Monitor key leakage
  • Implement rate limiting per key

Service Accounts

typescript
// Bad: Using personal developer token const client = new DatabaseClient({ token: "ghp_xxxx_personal_token", }); // Good: Using service account with limited scope const client = new DatabaseClient({ serviceAccountPath: "/var/run/secrets/database/service-account.json", scopes: ["database.read"], });

Monitoring and Observability

Essential Metrics

prometheus
# Prometheus metrics mcp_tool_invocations_total{tool="query_database", status="success"} 1234 mcp_tool_latency_seconds{tool="query_database", quantile="0.99"} 0.45
MetricWhy Monitor
Request rate and latencySudden changes may indicate attacks or degradation
Error ratesMCP errors and upstream failures
Resource consumptionCPU, memory, network I/O per instance
Authentication eventsLogin attempts, token refreshes, authorization failures

Structured Logging

json
{ "timestamp": "2026-01-15T10:23:45Z", "level": "info", "event": "tool_invocation", "tool": "query_database", "client_id": "claude-desktop-user-123", "arguments": { "query": "SELECT * FROM users WHERE email = ?" }, "duration_ms": 145, "status": "success", "result_size_bytes": 2048 }

Do not log sensitive data directly. Hash or redact PII, credentials, and other sensitive values:

typescript
function sanitizeForLogging(args: Record<string, unknown>) { const sensitive = ["password", "token", "secret", "key"]; return Object.fromEntries( Object.entries(args).map(([k, v]) => [ k, sensitive.some(s => k.toLowerCase().includes(s)) ? "[REDACTED]" : v, ]) ); }

Anomaly Detection

  • Unusual access patterns: Tool normally receiving 10 calls/hour suddenly receiving 1000
  • Unexpected tool combinations: If read_file is always followed by send_email from a particular client, and this pattern changes, alert
  • Geographic anomalies: Connections from clients in unexpected locations
  • Temporal anomalies: Activity outside normal hours

Multi-Server Orchestration

Avoiding Tool Collision

When multiple servers expose tools with similar names, clients might call the wrong one:

typescript
function detectCollisions(servers: Server[]): Collision[] { const toolMap = new Map<string, string[]>(); for (const server of servers) { for (const tool of server.tools) { const existing = toolMap.get(tool.name) || []; existing.push(server.name); toolMap.set(tool.name, existing); } } return Array.from(toolMap.entries()) .filter(([_, servers]) => servers.length > 1) .map(([tool, servers]) => ({ tool, servers })); }

Orchestration Patterns

Sequential Orchestration:

User → GitHub Server (get PR) → Code Analysis Server (analyze) → Response

Parallel Orchestration:

User → [GitHub Server, Jira Server, Slack Server] → Aggregate → Response

Conditional Orchestration:

User → Router → (if code-related) GitHub Server
              → (if data-related) Database Server
              → (if messaging-related) Slack Server

Cost Management

Rate Limiting

typescript
const rateLimiter = new RateLimiter({ perUser: { requests: 100, windowMs: 60000 }, perTool: { "expensive_analysis": { requests: 10, windowMs: 3600000 }, }, global: { requests: 10000, windowMs: 60000 }, }); app.use("/mcp", (req, res, next) => { const allowed = rateLimiter.check(req.user, req.tool); if (!allowed) { return res.status(429).json({ error: "Rate limit exceeded" }); } next(); });

Usage Tracking

typescript
interface CostEvent { timestamp: Date; userId: string; teamId: string; tool: string; resourceType: "api_call" | "compute" | "storage"; totalCost: number; } async function recordCost(event: CostEvent) { await costStore.insert(event); // Update totals for dashboards await updateDailyTotal(event.teamId, event.totalCost); // Alert if approaching budget const monthlySpend = await getMonthlySpend(event.teamId); const budget = await getBudget(event.teamId); if (monthlySpend > budget * 0.8) { await sendBudgetAlert(event.teamId, monthlySpend, budget); } }

Disaster Recovery

Recovery Procedures

ScenarioAction
Server CrashKubernetes restarts pods automatically. Verify with chaos engineering.
Database CorruptionRestore backup to point-in-time before corruption.
Credential CompromiseRotate all affected credentials. Revoke existing tokens. Audit logs.
Supply Chain CompromiseRollback to known good version. Audit activity during exposure window.

Bringing It Together

Production MCP deployment is not one thing—it is a combination of practices that together create reliability and security:

  1. Source from trusted registries with verification and scanning
  2. Containerize with security controls: signatures, network policies, resource limits
  3. Authenticate properly: OAuth for clients, service accounts for upstream
  4. Monitor everything: metrics, logs, anomaly detection
  5. Manage complexity: internal registries, collision detection, orchestration
  6. Control costs: rate limiting, usage tracking

No single control makes MCP safe. Together, they create defense in depth that makes attacks difficult and incidents manageable.


"Good architecture balances ambition with pragmatism."

— Hephaestus, The Strategist @ gsstk

Receive new articles

Subscribe to receive notifications about new articles directly to your email

We won't send spam. You can unsubscribe at any time.