
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,...
✨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.jsonmanifest - 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.
docker mcp gateway run \
--verify-signatures \
--block-network \
--log-calls \
--cpus 1 \
--memory 2GbWhen 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
| Registry | Best For | Key Benefit |
|---|---|---|
| Official MCP Registry | Canonical discovery, API access | Standardized metadata, federated architecture |
| Docker MCP Catalog | Secure deployment, local development | Signed images, vulnerability scanning |
| GitHub MCP Registry | Evaluation, open-source servers | Trust signals, community metrics |
Containerization Patterns
Basic Docker Deployment
docker run -d \
--name mcp-github \
-e GITHUB_TOKEN=$GITHUB_TOKEN \
mcp/github:latestSecurity-Hardened Deployment
docker mcp gateway run \
--verify-signatures \
--block-network \
--log-calls \
--cpus 1 \
--memory 2Gb| Flag | Function |
|---|---|
--verify-signatures | Only runs images with valid cryptographic signatures |
--block-network | Restricts outbound network access |
--log-calls | Logs every tool invocation |
--cpus 1 --memory 2Gb | Resource limits |
Kubernetes Deployment
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: 10Network Policies
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: 5432This 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:
- Client discovers OAuth endpoints via
/.well-known/oauth-authorization-server - Client generates PKCE code verifier and challenge
- Client redirects user to authorization endpoint
- User authenticates and grants permissions
- Server redirects back with authorization code
- Client exchanges code (plus PKCE verifier) for access tokens
- Client includes access token in MCP requests
API Keys for Simple Cases
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
// 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 metrics
mcp_tool_invocations_total{tool="query_database", status="success"} 1234
mcp_tool_latency_seconds{tool="query_database", quantile="0.99"} 0.45| Metric | Why Monitor |
|---|---|
| Request rate and latency | Sudden changes may indicate attacks or degradation |
| Error rates | MCP errors and upstream failures |
| Resource consumption | CPU, memory, network I/O per instance |
| Authentication events | Login attempts, token refreshes, authorization failures |
Structured Logging
{
"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:
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_fileis always followed bysend_emailfrom 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:
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) → ResponseParallel Orchestration:
User → [GitHub Server, Jira Server, Slack Server] → Aggregate → ResponseConditional Orchestration:
User → Router → (if code-related) GitHub Server
→ (if data-related) Database Server
→ (if messaging-related) Slack ServerCost Management
Rate Limiting
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
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
| Scenario | Action |
|---|---|
| Server Crash | Kubernetes restarts pods automatically. Verify with chaos engineering. |
| Database Corruption | Restore backup to point-in-time before corruption. |
| Credential Compromise | Rotate all affected credentials. Revoke existing tokens. Audit logs. |
| Supply Chain Compromise | Rollback 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:
- Source from trusted registries with verification and scanning
- Containerize with security controls: signatures, network policies, resource limits
- Authenticate properly: OAuth for clients, service accounts for upstream
- Monitor everything: metrics, logs, anomaly detection
- Manage complexity: internal registries, collision detection, orchestration
- 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