From Localhost to Production — 6 Steps

Saves 15+ hours — every config copy-paste ready, one afternoon to production.

Step 1: Local Development Server

FastMCP's dev mode gives you hot-reload and the MCP Inspector for testing. This is your local-only phase — no network exposure.

pip install "fastmcp>=3.0"
fastmcp dev server.py

The Inspector opens at http://localhost:5173 — use it to test every tool, resource, and prompt before deploying.

Step 2: HTTP Deployment with FastMCP v3

Switch from stdio to HTTP with the ASGI app pattern — gives you control over workers, middleware, and process management:

# app.py
from fastmcp import FastMCP

mcp = FastMCP("ProductionServer")

@mcp.tool()
def search_docs(query: str) -> str:
    """Search internal documentation."""
    return f"Found results for: {query}"

# Create ASGI app for uvicorn/gunicorn
app = mcp.http_app()
uvicorn app:app --host 127.0.0.1 --port 8000 --workers 4

Or with gunicorn for process management:

gunicorn app:app -w 4 -k uvicorn.workers.UvicornWorker --bind 127.0.0.1:8000

Your MCP endpoint is now at http://127.0.0.1:8000/mcp.

Step 3: Reverse Proxy with nginx + TLS

Never expose FastMCP directly to the internet. Put nginx in front with TLS.

# /etc/nginx/sites-available/mcp
server {
    listen 443 ssl http2;
    server_name mcp.yourcompany.com;

    ssl_certificate /etc/letsencrypt/live/mcp.yourcompany.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mcp.yourcompany.com/privkey.pem;

    location /mcp {
        proxy_pass http://127.0.0.1:8000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # MCP sessions can be long-lived
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
    }

    location /health {
        proxy_pass http://127.0.0.1:8000/health;
    }
}

server {
    listen 80;
    server_name mcp.yourcompany.com;
    return 301 https://$host$request_uri;
}
sudo certbot --nginx -d mcp.yourcompany.com
CORS Trap: If browser-based clients connect to your MCP server, you MUST set expose_headers:
app = mcp.http_app(
    cors_allow_origins=["https://your-app.com"],
    cors_expose_headers=["mcp-session-id"],  # Without this, JS clients break silently
)

Step 4: Authentication Layer

FastMCP v3 supports Bearer tokens, JWT, and OAuth. For team use, start with Bearer token auth:

from fastmcp.server.auth import BearerAuthProvider
import os

auth = BearerAuthProvider(
    token=os.environ["MCP_API_TOKEN"],
    jwt_secret=os.environ.get("JWT_SECRET"),  # Survives restarts
)

mcp = FastMCP("SecureServer", auth=auth)

Enterprise OAuth (Google/GitHub/Azure SSO)

from fastmcp.server.auth import OAuthProvider

auth = OAuthProvider(
    client_id=os.environ["OAUTH_CLIENT_ID"],
    client_secret=os.environ["OAUTH_CLIENT_SECRET"],
    authorize_url="https://accounts.google.com/o/oauth2/auth",
    token_url="https://oauth2.googleapis.com/token",
    jwt_secret=os.environ["JWT_SECRET"],
    token_store="redis://localhost:6379/0",
)

mcp = FastMCP("EnterpriseServer", auth=auth)
Critical: Without jwt_secret and token_store, tokens are generated in-memory and lost on every restart. Every client must re-authenticate. Set these explicitly.

Step 5: Docker Deployment

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN useradd -m mcpuser
USER mcpuser
EXPOSE 8000
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
# docker-compose.yml
services:
  mcp-server:
    build: .
    ports:
      - "127.0.0.1:8000:8000"
    environment:
      - MCP_API_TOKEN=${MCP_API_TOKEN}
      - JWT_SECRET=${JWT_SECRET}
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  nginx:
    image: nginx:alpine
    ports:
      - "443:443"
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
      - /etc/letsencrypt:/etc/letsencrypt:ro
    depends_on:
      - mcp-server
    restart: unless-stopped

Step 6: Kubernetes Deployment

Use K8s when you need auto-scaling, rolling deployments, or multi-region. For most teams, Docker Compose is enough.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: fastmcp-server
spec:
  replicas: 3
  selector:
    matchLabels:
      app: fastmcp-server
  template:
    metadata:
      labels:
        app: fastmcp-server
    spec:
      containers:
        - name: fastmcp
          image: your-registry/fastmcp-server:latest
          ports:
            - containerPort: 8000
          env:
            - name: MCP_API_TOKEN
              valueFrom:
                secretKeyRef:
                  name: mcp-secrets
                  key: api-token
            - name: JWT_SECRET
              valueFrom:
                secretKeyRef:
                  name: mcp-secrets
                  key: jwt-secret
          readinessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 5
            periodSeconds: 10
          resources:
            requests:
              memory: "256Mi"
              cpu: "250m"
            limits:
              memory: "512Mi"
              cpu: "500m"

Pair with an Ingress controller (nginx-ingress, Traefik) for TLS termination and routing.

FAQ

How do I monitor my MCP server in production?

Add a /health endpoint (the nginx config above already proxies it), then point UptimeRobot, BetterStack, or Prometheus at it. FastMCP exposes /mcp for API calls and /health for liveness checks by default. For metrics, wrap the ASGI app with prometheus-fastapi-instrumentator.

What happens to active sessions when the server restarts?

They're lost unless you set token_store (Redis) and jwt_secret. With both set, JWT tokens survive restarts and clients reconnect transparently. Without them, every client must re-authenticate after a restart.

Docker Compose vs Kubernetes — what should I start with?

Start with Docker Compose. It handles 95% of MCP deployments — single server, TLS via nginx, health checks, auto-restart. Graduate to Kubernetes when you need multi-region, auto-scaling, or you already run a K8s cluster. The overhead is rarely worth it for a single MCP server.

How do I handle TLS if I don't have a domain or can't use certbot?

Use a self-signed cert for internal/VPN deployments, or put Cloudflare Tunnel in front (free, no certbot needed). For public deployments without certbot: buy a cert, use ssl_certificate and ssl_certificate_key pointing to your CA-signed files — the nginx config is identical otherwise.

Save 20+ Hours

The Production Manual includes every config from this guide, plus 30+ error fixes, monitoring with Prometheus, and enterprise RBAC patterns. From $39, lifetime updates.

Save 20+ Hours