Configuring MCP transport protocols for Docker containers

Kashish Hora
Co-founder of 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 . .
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 /build/node_modules ./node_modules
COPY . .
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.
Related Guides
Configuring MCP installations for production deployments
Configure MCP servers for production with security, monitoring, and deployment best practices.
Building a stdio MCP server
Build MCP servers with stdio transport for local CLI tools and subprocess communication.
Setting up StreamableHTTP for scalable deployments
Deploy scalable MCP servers with StreamableHTTP for high performance and horizontal scaling.