Configuring MCP transport protocols for Docker containers

Kashish Hora

Kashish Hora

Co-founder of MCPcat

Try out MCPcat

The Quick Answer

Run MCP servers in Docker using stdio transport with interactive flags, or expose them via StreamableHTTP for remote access. The stdio approach requires -i flag for stdin communication, while HTTP-based transports need proper port mapping.

# stdio transport in Docker
$docker run --rm -i -v $(pwd)/data:/data your-mcp-server:latest
 
# StreamableHTTP with port exposure
$docker run -d -p 8080:8080 -e MCP_TRANSPORT=http your-mcp-server:latest

Prerequisites

Before configuring MCP transport protocols in Docker, ensure you have:

  • Docker Engine 20.10+ installed and running
  • Basic understanding of MCP transport protocols
  • An MCP server implementation ready to containerize
  • Docker Compose 2.0+ for multi-container setups (optional)

Installation

Prepare your MCP server for Docker deployment by creating appropriate container images. Start with a minimal Dockerfile that preserves transport flexibility:

# Dockerfile
FROM node:20-alpine
WORKDIR /app

# Install dependencies
COPY package*.json ./
RUN npm ci --only=production

# Copy server code
COPY . .

# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001
USER nodejs

# Default to stdio transport
ENV MCP_TRANSPORT=stdio
CMD ["node", "server.js"]

Build your container with multi-architecture support for broader compatibility:

# Build for multiple platforms
$docker buildx build --platform linux/amd64,linux/arm64 \
$ -t your-registry/mcp-server:latest .

Configuration

Docker containers require specific configurations based on your chosen MCP transport protocol. Each transport has unique networking and runtime requirements that affect how you structure your container deployment.

stdio Transport Configuration

The stdio transport uses standard input/output streams for communication. Docker requires the -i (interactive) flag to maintain stdin connectivity:

{
  "mcpServers": {
    "docker-server": {
      "command": "docker",
      "args": [
        "run",
        "--rm",
        "-i",
        "--name", "mcp-stdio",
        "-v", "$(pwd)/data:/data",
        "-e", "LOG_LEVEL=info",
        "your-mcp-server:latest"
      ]
    }
  }
}

Key configuration points for stdio transport:

  • Always use -i flag for interactive mode
  • Avoid -t (TTY) unless specifically needed
  • Use --rm to automatically clean up containers
  • Mount volumes for persistent data access

StreamableHTTP Transport Configuration

StreamableHTTP enables remote access through a single HTTP endpoint. Configure your container with proper port mapping and environment variables:

# docker-compose.yml
version: '3.8'

services:
  mcp-http:
    image: your-mcp-server:latest
    ports:
      - "8080:8080"
    environment:
      - MCP_TRANSPORT=http
      - PORT=8080
      - HOST=0.0.0.0
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 10s
      retries: 3
    restart: unless-stopped

The StreamableHTTP configuration requires exposing the service port and binding to all interfaces (0.0.0.0) within the container to accept external connections.

Network Isolation and Security

Configure Docker networks to isolate MCP servers while maintaining necessary connectivity:

# docker-compose.yml with network isolation
version: '3.8'

networks:
  mcp-network:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/16

services:
  mcp-server:
    image: your-mcp-server:latest
    networks:
      mcp-network:
        ipv4_address: 172.20.0.10
    ports:
      - "127.0.0.1:8080:8080"  # Bind only to localhost
    environment:
      - ALLOWED_ORIGINS=http://localhost:3000

This configuration restricts external access while allowing controlled communication between containers on the same network.

Usage

Deploy MCP servers in Docker with proper transport configuration for different scenarios. The deployment approach varies significantly between development and production environments.

Development Usage

For local development, use Docker Compose with volume mounting for hot reloading:

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

services:
  mcp-dev:
    build: 
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      - NODE_ENV=development
      - MCP_TRANSPORT=${MCP_TRANSPORT:-stdio}
    stdin_open: true  # For stdio transport
    tty: true
    ports:
      - "8080:8080"   # For HTTP transport
    command: npm run dev

Start the development container with your chosen transport:

# stdio transport (default)
$docker-compose -f docker-compose.dev.yml up
 
# StreamableHTTP transport
$MCP_TRANSPORT=http docker-compose -f docker-compose.dev.yml up

Production Deployment

Production deployments require additional considerations for reliability and security:

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

services:
  mcp-prod:
    image: your-registry/mcp-server:v1.2.3
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M
      restart_policy:
        condition: on-failure
        delay: 5s
        max_attempts: 3
    environment:
      - NODE_ENV=production
      - MCP_TRANSPORT=http
      - PORT=8080
      - LOG_LEVEL=warn
    ports:
      - "8080:8080"
    healthcheck:
      test: ["CMD", "node", "/app/healthcheck.js"]
      interval: 30s
      timeout: 3s
      retries: 3
      start_period: 40s

Deploy to production with rolling updates:

$docker stack deploy -c docker-compose.prod.yml mcp-stack

Hybrid Transport with stdio-to-SSE Proxy

Bridge stdio servers to network access using a proxy container:

# Run stdio server with SSE proxy
$docker run -d --name mcp-proxy \
$ -p 8000:8000 \
$ -e PROXY_PORT=8000 \
$ -e SSE_HOST=0.0.0.0 \
$ ghcr.io/sparfenyuk/mcp-proxy:latest \
$ docker run --rm -i your-mcp-server:latest

This approach maintains stdio simplicity while enabling remote access when needed.

Common Issues

Container Exits Immediately with stdio Transport

When a stdio-based MCP server container exits immediately, the issue stems from Docker's handling of stdin. Without the -i flag, Docker closes stdin, causing the server to terminate.

# Wrong - container exits immediately
$docker run --rm your-mcp-server:latest
 
# Correct - maintains stdin connection
$docker run --rm -i your-mcp-server:latest

The -i flag keeps stdin open even when not attached to a terminal. For debugging, add --init to ensure proper signal handling and process cleanup.

Port Binding Fails for StreamableHTTP

Port binding failures occur when the MCP server binds to localhost instead of all interfaces within the container. Docker's port mapping requires services to listen on 0.0.0.0:

// server.js - incorrect binding
const server = app.listen(8080, 'localhost'); // Won't work in Docker

// server.js - correct binding
const server = app.listen(8080, '0.0.0.0'); // Accessible from outside container

Always configure your MCP server to bind to 0.0.0.0 or use environment variables:

const host = process.env.HOST || '0.0.0.0';
const port = process.env.PORT || 8080;
const server = app.listen(port, host);

Volume Permissions with Non-Root User

Running containers as non-root users (recommended) can cause permission issues with mounted volumes. The host filesystem permissions don't match the container user:

# Fix permissions in Dockerfile
FROM node:20-alpine
WORKDIR /app

# Create app directory with correct ownership
RUN mkdir -p /data && chown -R node:node /app /data

# Switch to non-root user
USER node

# Copy files as node user
COPY --chown=node:node . .

For existing volumes, fix permissions at runtime:

services:
  mcp-server:
    image: your-mcp-server:latest
    user: "1000:1000"  # Match host user ID
    volumes:
      - ./data:/data:rw

Health Check Failures in Kubernetes

MCP servers deployed to Kubernetes may fail health checks due to incorrect probe configuration. Implement dedicated health endpoints:

// healthcheck.js
const http = require('http');

const checkHealth = () => {
  const options = {
    hostname: 'localhost',
    port: process.env.PORT || 8080,
    path: '/health',
    timeout: 2000,
  };

  const req = http.get(options, (res) => {
    process.exit(res.statusCode === 200 ? 0 : 1);
  });

  req.on('error', () => process.exit(1));
  req.on('timeout', () => process.exit(1));
};

checkHealth();

Configure Kubernetes probes appropriately:

livenessProbe:
  exec:
    command:
      - node
      - /app/healthcheck.js
  initialDelaySeconds: 30
  periodSeconds: 10

Examples

Example 1: Multi-Protocol MCP Server with Environment Detection

Build an MCP server that automatically selects the appropriate transport based on environment variables, supporting both local development and cloud deployment:

// server.js
const { StdioTransport } = require('@modelcontextprotocol/sdk');
const { StreamableHTTPTransport } = require('./transports/http');

const transport = process.env.MCP_TRANSPORT || 'stdio';
const server = new MCPServer();

async function startServer() {
  switch (transport) {
    case 'stdio':
      console.error('Starting stdio transport');
      const stdioTransport = new StdioTransport();
      await server.connect(stdioTransport);
      break;
      
    case 'http':
      console.error('Starting StreamableHTTP transport');
      const port = process.env.PORT || 8080;
      const httpTransport = new StreamableHTTPTransport(port);
      await server.connect(httpTransport);
      console.error(`Server listening on port ${port}`);
      break;
      
    default:
      throw new Error(`Unknown transport: ${transport}`);
  }
}

startServer().catch(console.error);

Deploy with Docker Compose for flexible transport selection:

# docker-compose.yml
version: '3.8'

services:
  mcp-flexible:
    build: .
    environment:
      - MCP_TRANSPORT=${TRANSPORT:-stdio}
      - PORT=8080
    # stdio configuration
    stdin_open: true
    # HTTP configuration  
    ports:
      - "${HOST_PORT:-8080}:8080"
    command: node server.js

Example 2: Load-Balanced StreamableHTTP Deployment

Deploy multiple MCP server instances behind an nginx load balancer for high availability:

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

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    depends_on:
      - mcp1
      - mcp2
      - mcp3

  mcp1:
    image: your-mcp-server:latest
    environment:
      - MCP_TRANSPORT=http
      - INSTANCE_ID=mcp1
    
  mcp2:
    image: your-mcp-server:latest
    environment:
      - MCP_TRANSPORT=http
      - INSTANCE_ID=mcp2
      
  mcp3:
    image: your-mcp-server:latest
    environment:
      - MCP_TRANSPORT=http
      - INSTANCE_ID=mcp3

Configure nginx for sticky sessions with health checking:

# nginx.conf
upstream mcp_backend {
    ip_hash;  # Sticky sessions
    server mcp1:8080 max_fails=3 fail_timeout=30s;
    server mcp2:8080 max_fails=3 fail_timeout=30s;
    server mcp3:8080 max_fails=3 fail_timeout=30s;
}

server {
    listen 80;
    
    location / {
        proxy_pass http://mcp_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header X-Real-IP $remote_addr;
        proxy_read_timeout 86400;
    }
    
    location /health {
        access_log off;
        return 200 "healthy\n";
    }
}

Example 3: Secure stdio Server with Resource Limits

Deploy a stdio MCP server with security constraints and resource limitations:

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

services:
  mcp-secure:
    image: your-mcp-server:latest
    stdin_open: true
    read_only: true  # Read-only root filesystem
    tmpfs:
      - /tmp
      - /app/temp
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    cap_add:
      - SETUID
      - SETGID
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 256M
          pids: 100
    environment:
      - NODE_ENV=production
      - LOG_LEVEL=warn
    volumes:
      - ./data:/data:ro
      - ./config:/config:ro
    user: "10001:10001"

Create a hardened Dockerfile for the secure deployment:

# Dockerfile.secure
FROM node:20-alpine AS builder
WORKDIR /build
COPY package*.json ./
RUN npm ci --only=production

FROM gcr.io/distroless/nodejs20-debian11
WORKDIR /app
COPY --from=builder /build/node_modules ./node_modules
COPY --chown=10001:10001 . .
USER 10001
EXPOSE 8080
CMD ["server.js"]

These examples demonstrate various deployment patterns for MCP servers in Docker, from simple single-container setups to complex high-availability configurations with proper security controls.