Dockerize Your MCP Server
Containerize your MCP server with Docker for consistent deployment, easy scaling, and production-ready packaging.
title: "Dockerize Your MCP Server" description: "Containerize your MCP server with Docker for consistent deployment, easy scaling, and production-ready packaging." order: 11 level: "advanced" duration: "20 min" keywords:
- MCP Docker
- Dockerize MCP server
- MCP container
- MCP Docker deployment
- MCP Dockerfile date: "2026-04-01"
Containerize your MCP server with Docker for consistent deployments. Learn to write optimized Dockerfiles with multi-stage builds, set up docker-compose for development and production, and configure containers for both stdio and SSE transports.
Why Docker for MCP Servers?
Docker provides reproducible builds, consistent environments, and simplified deployment. For MCP servers, Docker is especially useful for:
- SSE transport servers that need to run as long-lived services
- Servers with system dependencies (databases, native modules)
- Team collaboration where everyone needs the same environment
- Cloud deployments where containers are the standard unit
A Docker build technique that uses multiple FROM statements. The first stage compiles/builds the application, and the final stage copies only the built artifacts. This produces much smaller production images.
Dockerfile for a TypeScript MCP Server
Basic Dockerfile
Create Dockerfile in your project root:
FROM node:20-alpine AS builder
WORKDIR /app
# Install dependencies first (better cache utilization)
COPY package*.json ./
RUN npm ci
# Copy source and build
COPY tsconfig.json ./
COPY src/ ./src/
RUN npm run build
# Production stage
FROM node:20-alpine AS production
WORKDIR /app
# Copy only production dependencies and built files
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
# Non-root user for security
RUN addgroup -S mcp && adduser -S mcp -G mcp
USER mcp
# Default to stdio transport
CMD ["node", "dist/index.js"]
Always use multi-stage Docker builds for TypeScript MCP servers. The builder stage includes TypeScript, dev dependencies, and source code. The production stage only has the compiled JavaScript and runtime dependencies -- typically 70-80% smaller.
Optimized .dockerignore
Create .dockerignore:
node_modules
dist
.git
.env
*.md
.vscode
.DS_Store
Docker Compose for Development
Create docker-compose.yml:
services:
mcp-server:
build:
context: .
dockerfile: Dockerfile
ports:
- "3001:3001"
environment:
- PORT=3001
- NODE_ENV=production
env_file:
- .env
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3001/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
With a Database
For servers that need a database (like the database tutorial):
services:
mcp-server:
build: .
ports:
- "3001:3001"
environment:
- PORT=3001
- DATABASE_URL=postgresql://mcp:mcppass@postgres:5432/mcpdb
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
postgres:
image: postgres:16-alpine
environment:
- POSTGRES_USER=mcp
- POSTGRES_PASSWORD=mcppass
- POSTGRES_DB=mcpdb
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mcp"]
interval: 10s
timeout: 5s
retries: 5
volumes:
pgdata:
SSE Server Dockerfile
For SSE transport servers that need Express:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src/ ./src/
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
RUN addgroup -S mcp && adduser -S mcp -G mcp
USER mcp
EXPOSE 3001
ENV PORT=3001
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1
CMD ["node", "dist/index.js"]
Always add health checks to SSE server containers. This lets Docker (and container orchestrators like Kubernetes) know if your server is responsive and automatically restart it if it crashes.
Building and Running
Build the Docker image
docker build -t my-mcp-server .
Run with stdio transport
docker run --rm -i my-mcp-server
The -i flag is essential for stdio transport to work with Docker.
Run with SSE transport
docker run -d -p 3001:3001 --name mcp-server \
-e PORT=3001 \
-e API_KEY=your-key \
my-mcp-server
Run with docker-compose
docker compose up -d
docker compose logs -f mcp-server
Claude Desktop with Docker
stdio transport via Docker
{
"mcpServers": {
"docker-server": {
"command": "docker",
"args": ["run", "--rm", "-i", "my-mcp-server"]
}
}
}
SSE transport via Docker
{
"mcpServers": {
"docker-sse-server": {
"url": "http://localhost:3001/sse",
"transport": "sse"
}
}
}
Always create a non-root user in your Docker image and switch to it with the USER directive. This limits the damage if the container is compromised. The node user in Node.js base images works, or create a custom user as shown above.
Environment Variables and Secrets
Never bake secrets into your Docker image. Use environment variables:
# Via command line
docker run -e GITHUB_TOKEN=ghp_xxx my-mcp-server
# Via env file
docker run --env-file .env my-mcp-server
# Via docker-compose
docker compose --env-file .env.production up -d
Do not use ENV or ARG to embed API keys in your Dockerfile. They end up in the image layers and can be extracted. Always pass secrets at runtime via environment variables.
Image Size Comparison
| Approach | Image Size | Build Time |
|---|---|---|
| Single-stage (node:20) | ~1.2 GB | Fast |
| Single-stage (node:20-alpine) | ~400 MB | Fast |
| Multi-stage (node:20-alpine) | ~120 MB | Moderate |
| Multi-stage + pruning | ~80 MB | Slower |