# How to containerize an MCP server with Docker

- Tool: MCP
- Difficulty: Intermediate
- Time required: 20-30 min
- Compatibility: Docker 20+, MCP TypeScript SDK 1.x, MCP Python SDK 1.x
- Last updated: March 2026

## TL;DR

Package your MCP server in a Docker container for consistent deployment. Create a Dockerfile with the correct base image and build steps, then run the container with 'docker run -i' for stdio transport or with exposed ports for HTTP transport. Configure Claude Desktop and other clients to launch Docker-based MCP servers directly.

## Running MCP Servers in Docker Containers

Docker containers solve the 'works on my machine' problem for MCP servers. Instead of requiring users to install Node.js, Python, and dependencies, you package everything in a container image. Users just need Docker installed.

For stdio transport, Docker containers work seamlessly with MCP clients by running with the -i flag (interactive) to keep stdin open. For HTTP transport, you expose the server port. This tutorial covers Dockerfiles for both TypeScript and Python servers, client configuration, environment variable handling, and production deployment patterns.

## Before you start

- Docker Desktop or Docker Engine installed and running
- A working MCP server (TypeScript or Python) ready to containerize
- Basic familiarity with Docker concepts (images, containers, Dockerfile)
- An MCP client (Claude Desktop) for testing

## Step-by-step guide

### 1. Write a Dockerfile for a TypeScript MCP server

Create a multi-stage Dockerfile that installs dependencies, compiles TypeScript, and creates a minimal production image. Use a Node.js LTS base image and copy only the compiled output and production dependencies to the final stage. This keeps the image small and secure.

```
# Dockerfile for TypeScript MCP server
FROM node:22-slim AS builder

WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src/ ./src/
RUN npm run build

FROM node:22-slim AS runtime
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist

ENTRYPOINT ["node", "dist/index.js"]
```

**Expected result:** A Dockerfile that produces a small, production-ready image with only the compiled server and runtime dependencies.

### 2. Write a Dockerfile for a Python MCP server

For Python MCP servers, use a slim Python base image. Install dependencies from requirements.txt and copy the server file. Python does not need a build step, so the Dockerfile is simpler.

```
# Dockerfile for Python MCP server
FROM python:3.12-slim

WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY server.py ./

ENTRYPOINT ["python", "server.py"]
```

**Expected result:** A Dockerfile that packages the Python MCP server with all dependencies.

### 3. Build and run with stdio transport

Build the Docker image, then run it with 'docker run -i' to keep stdin open for the MCP protocol. The -i flag is critical — without it, the container's stdin is closed and the stdio transport cannot receive messages. Add --rm to clean up the container when it stops.

```
# Build the image
docker build -t my-mcp-server .

# Run with stdio transport (the -i flag is essential)
docker run -i --rm my-mcp-server

# With environment variables
docker run -i --rm \
  -e DATABASE_URL="postgresql://user:pass@host:5432/db" \
  -e API_KEY="sk-your-key" \
  my-mcp-server

# With a volume for local files
docker run -i --rm \
  -v /path/to/project:/data:ro \
  my-mcp-server
```

**Expected result:** The container starts, runs the MCP server on stdio, and can communicate with MCP clients through stdin/stdout.

### 4. Configure Claude Desktop to use a Docker MCP server

In Claude Desktop's configuration file, set the command to 'docker' and args to the run command. Claude Desktop launches the container as a child process, exactly like a local server, and communicates via stdio.

```
// claude_desktop_config.json
{
  "mcpServers": {
    "my-docker-server": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-e", "API_KEY=sk-your-key",
        "my-mcp-server"
      ]
    }
  }
}

// With volume mount for project access:
{
  "mcpServers": {
    "project-server": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-v", "/Users/me/project:/data:ro",
        "my-mcp-server"
      ]
    }
  }
}
```

**Expected result:** Claude Desktop launches the Docker container on startup and communicates with it via stdio.

### 5. Expose HTTP transport from Docker

For Streamable HTTP transport servers, expose the port with -p and run the container in the background with -d. Configure clients to connect via the HTTP URL instead of launching the container. This is useful for shared servers that multiple users connect to. For production Docker deployments of MCP servers, RapidDev can help set up container orchestration, health checks, and auto-scaling.

```
# Run HTTP transport server in background
docker run -d --rm \
  --name mcp-http-server \
  -p 3000:3000 \
  -e API_KEY="sk-your-key" \
  my-mcp-http-server

# Claude Desktop config for HTTP server
# {
#   "mcpServers": {
#     "remote-server": {
#       "url": "http://localhost:3000/mcp"
#     }
#   }
# }

# Health check
curl -s http://localhost:3000/health

# View logs
docker logs mcp-http-server

# Stop
docker stop mcp-http-server
```

**Expected result:** The HTTP MCP server runs in a Docker container accessible at localhost:3000.

## Complete code example

File: `Dockerfile`

```dockerfile
# Multi-stage Dockerfile for TypeScript MCP server
# Stage 1: Build
FROM node:22-slim AS builder
WORKDIR /app

# Install dependencies first (better layer caching)
COPY package.json package-lock.json ./
RUN npm ci

# Build TypeScript
COPY tsconfig.json ./
COPY src/ ./src/
RUN npm run build

# Stage 2: Production runtime
FROM node:22-slim
WORKDIR /app

# Production dependencies only
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force

# Copy compiled output
COPY --from=builder /app/dist ./dist

# Non-root user for security
RUN addgroup --system mcp && adduser --system --ingroup mcp mcp
USER mcp

# Health check for HTTP transport (optional)
# HEALTHCHECK --interval=30s --timeout=5s \
#   CMD curl -f http://localhost:3000/health || exit 1

# Stdio transport: no EXPOSE needed
# HTTP transport: EXPOSE 3000

ENTRYPOINT ["node", "dist/index.js"]
```

## Common mistakes

- **Running docker run without -i for stdio servers** — undefined Fix: The -i flag keeps stdin open. Without it, the container's stdin is immediately closed and the MCP server cannot receive messages.
- **Using -it instead of just -i** — undefined Fix: The -t flag allocates a TTY which can corrupt the binary MCP protocol. Use only -i for MCP stdio servers.
- **Hardcoding secrets in the Dockerfile** — undefined Fix: Pass secrets as environment variables with -e at runtime, never bake them into the image with ENV in the Dockerfile.
- **Not using multi-stage builds** — undefined Fix: Single-stage builds include TypeScript compiler, dev dependencies, and source code in the final image. Use multi-stage builds to keep images small.
- **Running as root inside the container** — undefined Fix: Add a non-root user (RUN adduser) and switch to it (USER mcp) for security.

## Best practices

- Use multi-stage Docker builds to keep images small (builder stage + runtime stage)
- Run containers as a non-root user for security
- Pass secrets via -e at runtime, never hardcode in Dockerfile
- Use -i (not -it) for stdio transport to avoid TTY corruption
- Pin base image versions (node:22-slim, python:3.12-slim) for reproducible builds
- Add .dockerignore for node_modules, .git, and other unnecessary files
- Add a health check endpoint for HTTP transport containers
- Use Docker Compose for servers that need databases or other services

## Frequently asked questions

### Why do I need -i when running Docker MCP servers?

The -i flag keeps the container's stdin open. MCP stdio transport reads JSON-RPC messages from stdin. Without -i, stdin is closed immediately and the server receives no input.

### Can I use Docker Compose for MCP servers?

Yes. Docker Compose is useful when your MCP server needs a database or other services. Define the server, database, and network in docker-compose.yml. However, stdio transport works best with standalone docker run.

### How do I update a Docker MCP server?

Rebuild the image (docker build -t my-mcp-server .), stop the old container, and restart. For stdio servers in Claude Desktop, restart Claude Desktop to pick up the new image.

### Can I publish my MCP server image to Docker Hub?

Yes. Tag and push: docker tag my-mcp-server username/my-mcp-server:latest && docker push username/my-mcp-server:latest. Users can then reference your image directly in their Claude Desktop config.

### How do I debug a Docker MCP server?

Check container logs: docker logs container-name. For stdio servers, add console.error() logging (which goes to Docker logs, not the MCP protocol). You can also run the server locally without Docker for debugging.

### What about network access from Docker containers?

By default, Docker containers can access the internet but not the host network. Use --network host (Linux) or host.docker.internal (Mac/Windows) to access host services like local databases. For complex networking setups with MCP servers, the RapidDev team can help configure the right Docker network topology.

---

Source: https://www.rapidevelopers.com/mcp-tutorial/how-to-use-mcp-server-with-docker
© RapidDev — https://www.rapidevelopers.com/mcp-tutorial/how-to-use-mcp-server-with-docker
