Deployment11 min read

Deploying MCP Servers with Docker: The Complete Guide

Learn how to containerize and deploy MCP servers using Docker. Covers Dockerfile setup, multi-stage builds, compose configurations, networking, security hardening, and production deployment patterns.

By MyMCPTools Team·

Docker is the most common deployment target for MCP servers in team and production environments. Containerizing your MCP server gives you consistent environments, easy scaling, and clean isolation from the host system — critical when your server has access to sensitive tools and data.

This guide walks through everything from a basic Dockerfile to production-ready multi-service deployments with Docker Compose.

Why Docker for MCP Servers?

MCP servers have specific reasons to containerize beyond the usual "it works on my machine" benefits:

  • Security isolation — MCP servers often have elevated access (filesystem, database, APIs). Docker namespacing and capabilities restrictions limit blast radius if a server is compromised.
  • Consistent tool versions — Node.js and Python MCP servers depend on specific SDK versions. Docker locks this in for every team member and deployment environment.
  • Multi-server orchestration — Running 5+ MCP servers? Docker Compose manages them as a unit with shared networking and volume mounts.
  • Resource limits — Prevent a runaway MCP process from consuming host resources with container CPU/memory limits.

Basic Dockerfile for a Node.js MCP Server

Most official MCP servers are Node.js packages. Here's a production-ready Dockerfile for a typical Node.js MCP server:

# Multi-stage build for smaller final image
FROM node:20-alpine AS builder

WORKDIR /app

# Copy package files first for layer caching
COPY package*.json ./
RUN npm ci --only=production

# Final stage
FROM node:20-alpine

# Run as non-root user (critical for security)
RUN addgroup -g 1001 -S mcpuser && \
    adduser -S mcpuser -u 1001

WORKDIR /app

# Copy only production dependencies
COPY --from=builder /app/node_modules ./node_modules
COPY --chown=mcpuser:mcpuser . .

USER mcpuser

# MCP servers communicate over stdio by default
CMD ["node", "dist/index.js"]

Dockerfile for the Official Filesystem MCP Server

The filesystem server needs a mounted volume to access your files. Here's a Dockerfile that exposes a configurable mount point:

FROM node:20-alpine

RUN addgroup -g 1001 -S mcpuser && \
    adduser -S mcpuser -u 1001

# Install the official server globally
RUN npm install -g @modelcontextprotocol/server-filesystem

# Create a workspace directory the container user can read
RUN mkdir -p /workspace && chown mcpuser:mcpuser /workspace

USER mcpuser

WORKDIR /workspace

ENTRYPOINT ["npx", "@modelcontextprotocol/server-filesystem"]
CMD ["/workspace"]

Run it with a volume mount:

docker run -v /your/project/path:/workspace \
  --read-only \
  --tmpfs /tmp \
  mcp-filesystem /workspace

Docker Compose for Multiple MCP Servers

In practice, you'll run multiple MCP servers simultaneously. Docker Compose is the right tool for this:

# docker-compose.yml
version: '3.9'

services:
  mcp-filesystem:
    image: mcp-filesystem:latest
    build:
      context: ./servers/filesystem
    volumes:
      - ./workspace:/workspace:ro
    environment:
      - NODE_ENV=production
    restart: unless-stopped
    read_only: true
    tmpfs:
      - /tmp
    cap_drop:
      - ALL

  mcp-postgres:
    image: mcp-postgres:latest
    build:
      context: ./servers/postgres
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/mydb
    networks:
      - mcp-internal
      - db-network
    depends_on:
      - postgres
    restart: unless-stopped

  mcp-brave-search:
    image: mcp-brave-search:latest
    build:
      context: ./servers/brave-search
    environment:
      - BRAVE_API_KEY=${BRAVE_API_KEY}
    restart: unless-stopped

  postgres:
    image: postgres:16-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=mydb
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    networks:
      - db-network

networks:
  mcp-internal:
    driver: bridge
    internal: true
  db-network:
    driver: bridge

volumes:
  postgres_data:

Connecting Containerized MCP Servers to Claude Desktop

Claude Desktop and other MCP clients communicate with MCP servers over stdio. For containerized servers, you need to wrap the docker run command:

// claude_desktop_config.json
{
  "mcpServers": {
    "filesystem": {
      "command": "docker",
      "args": [
        "run", "--rm", "-i",
        "-v", "/Users/you/projects:/workspace:ro",
        "mcp-filesystem:latest",
        "/workspace"
      ]
    },
    "postgres": {
      "command": "docker",
      "args": [
        "run", "--rm", "-i",
        "--network", "mcp-compose_mcp-internal",
        "-e", "DATABASE_URL=postgresql://user:pass@postgres:5432/mydb",
        "mcp-postgres:latest"
      ]
    }
  }
}

The -i flag keeps stdin open, which is required for stdio-based MCP communication. The --rm flag removes the container after each session.

Security Hardening for Production

MCP servers with sensitive access need hardened containers:

# docker-compose.yml (security-hardened service)
services:
  mcp-sensitive:
    image: mcp-sensitive:latest
    read_only: true
    tmpfs:
      - /tmp:size=50m
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE
    security_opt:
      - no-new-privileges:true
    mem_limit: 256m
    cpus: '0.5'
    user: "1001:1001"
    networks:
      - mcp-isolated

networks:
  mcp-isolated:
    driver: bridge
    internal: true

Environment Variable Management

Never bake API keys into Docker images. Use environment files with Docker Compose:

# .env (never commit this file)
BRAVE_API_KEY=BSA...
POSTGRES_PASSWORD=secure_password_here
GITHUB_TOKEN=ghp_...

# .env.example (commit this)
BRAVE_API_KEY=your_brave_api_key
POSTGRES_PASSWORD=your_db_password
GITHUB_TOKEN=your_github_token

For production deployments, use Docker secrets instead of environment variables for sensitive values:

# docker-compose.yml with secrets
services:
  mcp-postgres:
    secrets:
      - db_password
    environment:
      - DATABASE_PASSWORD_FILE=/run/secrets/db_password

secrets:
  db_password:
    external: true

Health Checks and Restart Policies

MCP servers can hang silently. Add health checks and appropriate restart policies:

services:
  mcp-filesystem:
    healthcheck:
      test: ["CMD", "node", "-e", "require('fs').accessSync('/workspace')"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 10s
    restart: unless-stopped
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

Multi-Architecture Builds for Team Environments

If your team mixes Apple Silicon and x86 machines, build multi-arch images:

# Build for both arm64 and amd64
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --tag yourregistry/mcp-filesystem:latest \
  --push \
  ./servers/filesystem

Debugging Containerized MCP Servers

When an MCP server behaves unexpectedly in a container:

# Run interactively to test stdio communication
docker run --rm -it \
  -v /your/project:/workspace:ro \
  mcp-filesystem:latest \
  /workspace

# Check logs for a running compose service
docker compose logs -f mcp-filesystem

# Inspect the running container
docker exec -it mcp-filesystem-container sh

Next Steps

Once you're comfortable with Docker deployments, consider moving to a managed container platform for zero-downtime restarts and auto-scaling. See our guides: Deploying MCP to AWS Lambda, Deploying MCP to Railway, and MCP Server Security Best Practices.

Browse the full MCP server directory to find servers ready for Docker deployment.

Recommended Tools

Better Stack

Free Plan

Get alerted when your APIs, browser tests, payment pipelines, or MCP server dependencies go down. Used by 100K+ developers.

Start monitoring free →

1Password

14-day Free Trial

Store and inject API keys, payment credentials, tokens, and file access secrets into your MCP server configs. Trusted by 150K+ developers.

Try 1Password free →

🔧 MCP Servers Mentioned in This Article

📁

Filesystem MCP Server

sandboxed read, write, edit, move and search access to an explicit whitelist of local directories, and it is the reference implementation most other filesystem MCP servers are modelled on. Shipped by Anthropic in the official modelcontextprotocol/servers monorepo (89,000+ stars, actively maintained), it is a Node.js server published to npm as @modelcontextprotocol/server-filesystem. The part worth understanding before you install is the access-control model, because there are now two ways to grant directories and they do not compose. Method one is command-line arguments: `npx -y @modelcontextprotocol/server-filesystem /path/one /path/two`. Method two, and the one the maintainers recommend, is MCP Roots — a client that supports the roots protocol sends its roots at initialization, and those roots COMPLETELY REPLACE any directories passed on the command line, then get replaced again on every `notifications/roots/list_changed`. That means allowed directories can change at runtime without restarting the server, but it also means a roots-capable client silently overrides your CLI arguments. If the server starts with no arguments and the client either does not support roots or sends an empty list, initialization throws an error. The tool surface is broad: `read_text_file` (with mutually exclusive `head`/`tail` line windows), `read_media_file` returning base64 image/audio content blocks, `read_multiple_files` which keeps going when individual reads fail, `write_file`, `edit_file`, `create_directory`, `list_directory`, `list_directory_with_sizes`, `move_file`, `search_files`, `directory_tree`, `get_file_info` and `list_allowed_directories`. `edit_file` is the one to learn — it does line-based and multi-line pattern matching with indentation detection and preservation, returns a git-style diff with context, and supports `dryRun: true` so you can preview a change before applying it; the maintainers recommend always running a dry run first. Every operation is refused outside the allowed set, and `list_allowed_directories` is the fastest way to confirm what the server actually believes it can touch.

Local
🗄️

Redis MCP Server

The Redis MCP Server (redis/mcp-redis) is Redis's own natural-language interface for agentic applications, letting an AI client read and write Redis data over the Model Context Protocol. Note which one you install: the server most tutorials still point at is Anthropic's reference implementation, which now lives in modelcontextprotocol/servers-archived, and its npm package @modelcontextprotocol/server-redis is explicitly marked "Package no longer supported" with a last publish of 2025-04-25. The maintained server is a Python package instead, run with uvx --from redis-mcp-server@latest, and it covers far more of Redis than the reference one did: string, hash, list, set and sorted-set tools; JSON document tools; pub/sub with stateful channel and pattern subscriptions; Streams tools including consumer-group create, read, acknowledge and destroy; vector index management and vector search through the query engine; a docs search tool; and a server-management tool for database info. Connection is a redis:// or rediss:// URL passed as --url, or the REDIS_HOST/REDIS_PORT/REDIS_PWD/REDIS_SSL environment variables, with Redis Cluster mode behind REDIS_CLUSTER_MODE and EntraID service-principal, managed-identity and default-credential auth flows for Azure Managed Redis. There is no --read-only flag: the documented way to stop an agent writing is a Redis ACL user (ACL SETUSER readonlyuser on >pw ~* +@read -@write). Ships as a PyPI package, a GitHub install via uvx, and an official mcp/redis Docker image; stdio transport only.

Local📘
🔍

Brave Search MCP Server

The Brave Search MCP Server is the official server from Brave that gives AI assistants privacy-first web search through the independent Brave Search API — no tracking, no profiling, and results drawn from Brave's own web index rather than Google or Bing. It exposes five distinct tools that map directly to the Brave Search API endpoints: brave_web_search for general queries with pagination, freshness filters, and safe-search controls; brave_local_search for businesses, restaurants, and points of interest with automatic location filtering; brave_news_search for recent articles and current events; brave_image_search for image discovery; and brave_video_search for finding videos across the web. Authentication uses a single BRAVE_API_KEY (free tier available at brave.com/search/api) or a mounted BRAVE_API_KEY_FILE for Docker-secret setups. Install in Claude Desktop, Cursor, Windsurf, or VS Code with one npx command and choose stdio or streamable-HTTP transport. Because Brave operates its own crawler and index, the Brave Search MCP server is a strong choice for developers who want an alternative to Google-dependent search tools, need reproducible non-personalized results, or care about data privacy in agent workflows — Claude can pull fresh web context, verify facts, and research topics without leaking queries to ad-tech pipelines.

Local

📚 More from the Blog