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

> How to containerize and deploy an MCP server for production. Covers Dockerfile, Docker Compose, StreamableHTTP transport, security hardening, and scaling patterns for cloud deployment.

**Source:** https://mcpplaygroundonline.com/blog/deploy-mcp-server-docker-production-guide  
**Author:** Nikhil Tiwari  
**Published:** 2026-02-13  
**Updated:** 2026-02-13  
**Category:** Tutorial  
**Reading time:** 14 min read

---

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](https://www.docker.com/blog/build-to-prod-mcp-servers-with-docker/)

StreamableHTTP reference (stateful)

[github.com/yigitkonur/example-mcp-server-streamable-http](https://github.com/yigitkonur/example-mcp-server-streamable-http)

StreamableHTTP reference (stateless)

[github.com/yigitkonur/...stateless](https://github.com/yigitkonur/example-mcp-server-streamable-http-stateless)

Docker MCP Gateway

[docs.docker.com/ai/mcp-gateway](https://docs.docker.com/ai/mcp-gateway)

MCP Python SDK

[github.com/modelcontextprotocol/python-sdk](https://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 →](/mcp-test-server)

## Related Content

-   [Build Your First MCP Server with Python and FastMCP](/blog/build-mcp-server-python-fastmcp-tutorial)
-   [Remote MCP Servers — Test and Connect Online](/blog/remote-mcp-servers)
-   [MCP vs Function Calling vs APIs](/blog/mcp-vs-function-calling-vs-api-comparison)
-   [MCP vs A2A — Model Context Protocol vs Agent2Agent](/blog/mcp-vs-a2a-agent2agent-protocol)
-   [MCP Security: Tool Poisoning and OWASP Top 10](/blog/mcp-security-tool-poisoning-owasp-top-10-mcp-scan)

## 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.

---

_Canonical page: https://mcpplaygroundonline.com/blog/deploy-mcp-server-docker-production-guide — MCP Playground (mcpplaygroundonline.com), the free browser-based tool for testing MCP servers and building AI agents._
