For a solo developer, npx-based MCP servers are fine. For a team or a production agent, Docker is the right layer. Here is why — and the config template we use ourselves.
Why Docker, specifically
- Version pinning —
ghcr.io/vendor/mcp:v1.4.2is deterministic.npx -y packagefetches latest on every launch. - Language isolation — a Python MCP server does not need Python on your host.
- Network scoping — you can block outbound network except to specific hosts via
--networkrules. - Secret rotation — env vars passed per launch; you don’t leave tokens in config files.
- Team sync — commit a
docker-compose.ymlto the repo; everyone runs the same version.
Minimal Dockerized config in Claude Desktop
{
"mcpServers": {
"github": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"-e", "GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server:v1.4.2"
],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_..."
}
}
}
}
Key flags:
--rm— container removed after exit, no state drift-i— interactive stdio (required for MCP stdio transport)-e VAR— pass env var by name, value supplied separately. Token never appears inpsoutput.
Pin the tag. Always.
Using ghcr.io/vendor/mcp:latest guarantees a silent breakage the moment the vendor publishes a major version. Pin to a semver tag and bump explicitly:
ghcr.io/github/github-mcp-server:v1.4.2 // ✔
ghcr.io/github/github-mcp-server:latest // ❌
Volume mounts for local data
For Filesystem MCP or SQLite MCP, mount the host dir:
{
"filesystem": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"-v", "/Users/you/projects:/workspace:rw",
"mcp/filesystem:v1.0.0",
"/workspace"
]
}
}
:ro instead of :rw for read-only mounts when you want the agent to look but not touch.
Network scoping (important for security)
Default Docker bridge gives full internet. Restrict with:
--network none— no outbound at all. Fine for filesystem / SQLite servers.- Custom bridge with iptables rules — only allow
api.github.com, etc.
Resource limits
Prevent a rogue server from eating your laptop:
--memory=512m --cpus=1.0
Team workflow: share config via Git
Commit mcp/servers.d/*.json and a script that merges them into claude_desktop_config.json:
#!/bin/sh
jq -s '.[0] * { mcpServers: ([.[].mcpServers] | add) }' \
mcp/servers.d/*.json > ~/Library/Application\ Support/Claude/claude_desktop_config.json
New hire runs the script, gets the full team stack.
Rolling your own MCP image
Minimal Dockerfile for a Node-based MCP server:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY dist ./dist
USER node
ENTRYPOINT ["node", "dist/index.js"]
Build, push, reference by tag:
docker build -t ghcr.io/yourco/mcp-internal:v0.1.0 .
docker push ghcr.io/yourco/mcp-internal:v0.1.0