Production-ready Docker deployment with security hardening and monitoring.

Quick Start

# Build and run
docker build -t mcp-server .
docker run -p 8080:8080 -p 9090:9090 mcp-server

Production Docker Configuration

Dockerfile Features

Security Hardened

Production Optimized

Build Options

Standard Build (Recommended):

# Using UV Python image
docker build -t mcp-server .

Alternative Build:

# Using standard Python image
docker build -f Dockerfile.alternative -t mcp-server .

Environment Configuration

Required Environment Variables

# Server Configuration
MCP_SERVER_NAME=production-server
MCP_TRANSPORT=http
MCP_HOST=0.0.0.0
MCP_PORT=8080

# Security
MCP_RATE_LIMIT=1000/minute
WORKSPACE_PATH=/workspace

# Monitoring
MCP_METRICS_PORT=9090

Production .env File

# Server Identity
MCP_SERVER_NAME=my-production-server
MCP_TRANSPORT=http
MCP_HOST=0.0.0.0
MCP_PORT=8080

# Security Configuration
MCP_RATE_LIMIT=1000/minute
WORKSPACE_PATH=/app/workspace

# Monitoring & Observability
MCP_METRICS_PORT=9090
PYTHONUNBUFFERED=1

# Resource Limits
MCP_MAX_FILE_SIZE=10485760
MCP_COMMAND_TIMEOUT=30

Docker Compose Deployment

Basic Production Stack

# docker-compose.yml
version: '3.8'

services:
  mcp-server:
    build: .
    ports:
      - "8080:8080"
      - "9090:9090"
    environment:
      - MCP_SERVER_NAME=production-server
      - MCP_TRANSPORT=http
      - MCP_HOST=0.0.0.0
      - MCP_RATE_LIMIT=1000/minute
    volumes:
      - ./workspace:/workspace
      - ./logs:/tmp/mcp-logs
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "python", "-c", "from fastmcp import FastMCP; import sys; sys.exit(0)"]
      interval: 30s
      timeout: 10s
      retries: 3
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '0.5'
    security_opt:
      - no-new-privileges:true
    read_only: true
    tmpfs:
      - /tmp:noexec,nosuid,size=100m

Full Production Stack with Monitoring

# docker-compose.prod.yml
version: '3.8'

services:
  mcp-server:
    build: .
    ports:
      - "8080:8080"
    environment:
      - MCP_SERVER_NAME=production-server
      - MCP_TRANSPORT=http
      - MCP_HOST=0.0.0.0
      - MCP_RATE_LIMIT=1000/minute
      - MCP_METRICS_PORT=9090
    volumes:
      - workspace:/workspace
      - logs:/tmp/mcp-logs
    restart: unless-stopped
    networks:
      - mcp-network
    depends_on:
      - prometheus

  prometheus:
    image: prom/prometheus:v2.45.0
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
      - '--web.console.libraries=/etc/prometheus/console_libraries'
      - '--web.console.templates=/etc/prometheus/consoles'
      - '--storage.tsdb.retention.time=200h'
      - '--web.enable-lifecycle'
    networks:
      - mcp-network

  grafana:
    image: grafana/grafana:10.0.0
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=secure-password-here
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/dashboards:/etc/grafana/provisioning/dashboards
      - ./grafana/datasources:/etc/grafana/provisioning/datasources
    networks:
      - mcp-network

  nginx:
    image: nginx:1.25-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - mcp-server
    networks:
      - mcp-network

volumes:
  workspace:
  logs:
  prometheus_data:
  grafana_data:

networks:
  mcp-network:
    driver: bridge

Security Configuration

Container Security

Security Options:

security_opt:
  - no-new-privileges:true    # Prevent privilege escalation
  - apparmor:docker-default   # Use AppArmor profile
  - seccomp:default          # Use seccomp profile

# Read-only filesystem
read_only: true
tmpfs:
  - /tmp:noexec,nosuid,size=100m

User Security:

# Dockerfile includes
RUN useradd -m -s /bin/bash mcpuser
USER mcpuser

# Runtime override if needed
docker run --user 1000:1000 mcp-server

Network Security

Internal Network:

networks:
  mcp-internal:
    driver: bridge
    internal: true  # No external access

Reverse Proxy Configuration (nginx.conf):

upstream mcp-backend {
    server mcp-server:8080;
}

server {
    listen 443 ssl http2;
    server_name your-domain.com;
    
    # SSL Configuration
    ssl_certificate /etc/nginx/ssl/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512;
    
    # Security Headers
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;
    add_header X-XSS-Protection "1; mode=block";
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
    
    # Rate Limiting
    limit_req_zone $binary_remote_addr zone=mcp:10m rate=10r/s;
    limit_req zone=mcp burst=20 nodelay;
    
    location / {
        proxy_pass http://mcp-backend;
        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;
        
        # Timeouts
        proxy_connect_timeout 5s;
        proxy_send_timeout 10s;
        proxy_read_timeout 30s;
    }
    
    # Metrics endpoint (restrict access)
    location /metrics {
        allow 10.0.0.0/8;   # Internal networks only
        allow 172.16.0.0/12;
        allow 192.168.0.0/16;
        deny all;
        
        proxy_pass http://mcp-backend:9090;
    }
}

Monitoring Setup

Prometheus Configuration

# prometheus.yml
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'mcp-server'
    static_configs:
      - targets: ['mcp-server:9090']
    metrics_path: /metrics
    scrape_interval: 5s

  - job_name: 'docker'
    static_configs:
      - targets: ['host.docker.internal:9323']

Grafana Dashboard

Key metrics to monitor:

{
  "dashboard": {
    "title": "MCP Server Metrics",
    "panels": [
      {
        "title": "Request Rate",
        "targets": [
          {
            "expr": "rate(mcp_requests_total[5m])"
          }
        ]
      },
      {
        "title": "Error Rate", 
        "targets": [
          {
            "expr": "rate(mcp_requests_total{status=\"error\"}[5m])"
          }
        ]
      },
      {
        "title": "Request Duration",
        "targets": [
          {
            "expr": "histogram_quantile(0.95, rate(mcp_request_duration_seconds_bucket[5m]))"
          }
        ]
      },
      {
        "title": "Rate Limit Violations",
        "targets": [
          {
            "expr": "rate(mcp_requests_total{status=\"rate_limited\"}[5m])"
          }
        ]
      }
    ]
  }
}

Deployment Commands

Development

# Build and run for development
docker build -t mcp-server-dev .
docker run -it --rm \
  -p 8080:8080 \
  -p 9090:9090 \
  -v $(pwd):/workspace \
  -e MCP_TRANSPORT=http \
  mcp-server-dev

Staging

# Deploy to staging
docker-compose -f docker-compose.staging.yml up -d

# Check logs
docker-compose logs -f mcp-server

# Run security scan
docker exec mcp-server python security_scan.py

Production

# Deploy to production
docker-compose -f docker-compose.prod.yml up -d

# Health check
curl -f http://localhost:8080/health || exit 1

# Monitoring check
curl -f http://localhost:9090/metrics | head -20

Health Checks and Monitoring

Docker Health Check

HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD python -c "from fastmcp import FastMCP; import sys; sys.exit(0)" || exit 1

External Health Monitoring

#!/bin/bash
# health-check.sh

HEALTH_URL="http://localhost:8080/health"
METRICS_URL="http://localhost:9090/metrics"

# Check health endpoint
if ! curl -f -s "$HEALTH_URL" > /dev/null; then
    echo "Health check failed"
    exit 1
fi

# Check metrics endpoint  
if ! curl -f -s "$METRICS_URL" | grep -q "mcp_requests_total"; then
    echo "Metrics check failed"
    exit 1
fi

echo "All checks passed"

Troubleshooting

Common Issues

Container won’t start:

# Check logs
docker logs <container-id>

# Check resource usage
docker stats <container-id>

# Inspect configuration
docker inspect <container-id>

Port conflicts:

# Check what's using the port
netstat -tulpn | grep :8080

# Use different port
docker run -p 8081:8080 mcp-server

Permission errors:

# Fix file ownership
sudo chown -R 1000:1000 ./workspace

# Run with specific user
docker run --user 1000:1000 mcp-server

Memory issues:

# Set memory limits
docker run -m 512m mcp-server

# Monitor memory usage
docker stats --format "table \t\t"

Performance Optimization

Multi-stage builds for smaller images:

# Build stage
FROM python:3.12-slim as builder
COPY requirements.txt .
RUN pip install --user -r requirements.txt

# Production stage  
FROM python:3.12-slim
COPY --from=builder /root/.local /root/.local
COPY . /app

Resource limits:

deploy:
  resources:
    limits:
      memory: 512M
      cpus: '0.5'
    reservations:
      memory: 256M
      cpus: '0.25'

Security Scanning

Run security scans on containers:

# Scan image for vulnerabilities
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
  -v $HOME/Library/Caches:/root/.cache/ \
  aquasec/trivy:latest image mcp-server

# Run application security scan
docker exec mcp-server python security_scan.py

For advanced Kubernetes deployment, see the Kubernetes Guide.