Back to Blog
TutorialFeb 13, 202614 min read

Deploy an MCP Server to Production with Docker (Complete Guide)

NT

Nikhil Tiwari

MCP Playground

TL;DR

  • Use StreamableHTTP transport for remote/production MCP servers (not stdio)
  • Containerize with Docker for isolation, portability, and reproducible deploys
  • Expose a single /mcp endpoint that handles POST (requests) and GET+SSE (streaming)
  • Add TLS, CORS, auth, and rate limiting before going to production

Most MCP tutorials show stdio servers — great for local use with Claude Desktop or Cursor. But when you want your MCP server accessible over the network — for remote agents, team use, or production workloads — you need HTTP transport, a container, and proper infrastructure.

This guide takes you from a local MCP server to a production-ready Docker deployment.

Why Docker for MCP Servers?

Isolation

Dependencies contained. No conflicts with host system.

Portability

Same image runs on laptop, cloud VM, or Kubernetes.

Security

Sandboxes server from host filesystem. Limits blast radius.

Reproducibility

Share a Dockerfile, not installation docs.

Step 1: Create the MCP Server (Python)

A simple server with two tools, using the MCP Python SDK's built-in FastMCP:

# server.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("Production Server")

@mcp.tool()
def search_docs(query: str) -> str:
    """Search internal documentation."""
    # Replace with your actual search logic
    return f"Found 3 results for '{query}'"

@mcp.tool()
def get_status(service: str) -> dict:
    """Get the status of an internal service."""
    return {"service": service, "status": "healthy", "uptime": "99.9%"}

if __name__ == "__main__":
    mcp.run(transport="streamable-http", host="0.0.0.0", port=8000)

Key: Use transport="streamable-http" instead of the default stdio. This exposes a /mcp HTTP endpoint.

# requirements.txt
mcp[cli]>=1.0.0
uvicorn

Step 2: Write the Dockerfile

# Dockerfile
FROM python:3.12-slim

WORKDIR /app

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy server code
COPY server.py .

# Expose the MCP port
EXPOSE 8000

# Run the server
CMD ["python", "server.py"]

Step 3: Build and Run

# Build the image
docker build -t my-mcp-server .

# Run the container
docker run -d -p 8000:8000 --name mcp-server my-mcp-server

# Test it's working
curl -X POST http://localhost:8000/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'

Step 4: Docker Compose (With Dependencies)

Most production servers need a database, cache, or other services. Use Docker Compose:

# docker-compose.yml
version: "3.8"

services:
  mcp-server:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://app:secret@postgres:5432/mcpdata
      - API_KEY=${API_KEY}
    depends_on:
      postgres:
        condition: service_healthy
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/mcp"]
      interval: 30s
      timeout: 10s
      retries: 3

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: mcpdata
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d mcpdata"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  pgdata:
# Start everything
docker compose up -d

# Check logs
docker compose logs -f mcp-server

Step 5: Connect Clients

Once running, any MCP client can connect to your server via HTTP:

Cursor

{
  "mcpServers": {
    "my-server": {
      "url": "http://localhost:8000/mcp"
    }
  }
}

Claude Code

claude mcp add --transport http my-server http://localhost:8000/mcp

OpenAI Agents SDK

from agents.mcp import MCPServerStreamableHttp

async with MCPServerStreamableHttp(
    name="my-server",
    params={"url": "http://localhost:8000/mcp"},
) as server:
    # Use server with an agent

Production Hardening

1. Add a Reverse Proxy (Nginx)

# nginx.conf
server {
    listen 443 ssl;
    server_name mcp.yourcompany.com;

    ssl_certificate /etc/ssl/certs/cert.pem;
    ssl_certificate_key /etc/ssl/private/key.pem;

    location /mcp {
        proxy_pass http://mcp-server: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;
    }
}

2. Security Checklist

Concern Action
TLS Always use HTTPS in production. Terminate TLS at Nginx or your load balancer.
Authentication Add Bearer token or OAuth validation. Check the Authorization header.
CORS Restrict Access-Control-Allow-Origin to known clients.
Rate limiting Add rate limits at the Nginx or application level to prevent abuse.
Input validation Validate all tool parameters. Sanitise inputs before database queries or shell commands.
Secrets Use Docker secrets or environment variables. Never bake credentials into images.
Non-root user Add USER 1000 in Dockerfile. Don't run as root inside containers.

3. Stateful vs Stateless

Pattern When to use Session storage
Stateless Serverless, auto-scaling, Kubernetes. Each request is independent. None — fresh instance per request
Stateful Multi-step workflows, persistent context across tool calls. Redis, database, or in-memory with sticky sessions

For stateful deployments with multiple replicas, use Redis for session storage and configure your load balancer for sticky sessions.

Cloud Deployment Options

Platform How Notes
AWS ECS / Fargate Push image to ECR, create task definition, run as service Good for auto-scaling. Use ALB for load balancing.
Google Cloud Run Push image, deploy with gcloud run deploy Scales to zero. Good for stateless servers.
Azure Container Apps Deploy container image with Azure CLI Managed Kubernetes under the hood.
Railway / Render / Fly.io Connect Git repo or push Dockerfile Simplest. Good for small teams.
Kubernetes Deployment + Service + Ingress manifests Full control. Use for multi-server setups.

Key Resources

Resource Link
Docker MCP blog post docker.com/blog/build-to-prod-mcp-servers-with-docker
StreamableHTTP reference (stateful) github.com/yigitkonur/example-mcp-server-streamable-http
StreamableHTTP reference (stateless) github.com/yigitkonur/...stateless
Docker MCP Gateway docs.docker.com/ai/mcp-gateway
MCP Python SDK github.com/modelcontextprotocol/python-sdk

Test Your Deployed MCP Server

Paste your server URL into MCP Playground to verify it's working

Open MCP Playground →

Related Content

Frequently Asked Questions

Can I deploy an MCP server without Docker?
Yes. You can run the Python script directly on a VM, use systemd to manage it, and put Nginx in front. Docker just makes it easier to package, deploy, and reproduce. For serverless, you can deploy to Cloud Run or AWS Lambda with the appropriate adapter.
What's the difference between stdio and StreamableHTTP transport?
Stdio communicates over standard input/output — the client launches the server as a local subprocess. It only works locally. StreamableHTTP uses HTTP requests to a /mcp endpoint, allowing remote access over the network. Use stdio for local development, StreamableHTTP for production and remote access.
How do I handle secrets in Docker?
Pass secrets via environment variables (docker run -e API_KEY=xxx) or Docker secrets. Never put credentials in your Dockerfile or image. In Docker Compose, use environment with variable substitution (${API_KEY}) and keep actual values in a .env file that is not committed to git.
Should I use stateful or stateless mode?
Start with stateless — it's simpler, scales better, and works with serverless platforms. Use stateful only if your tools need context from previous calls in the same session (e.g., multi-step workflows). Stateful mode requires sticky sessions or a shared session store like Redis.
NT

Written by Nikhil Tiwari

15+ years in product development. AI enthusiast building developer tools that make complex technologies accessible to everyone.