docs/docker-isolation.md
MCPProxy provides Docker isolation for stdio MCP servers to enhance security by running each server in its own isolated container.
New installs: Docker isolation is turned on automatically when mcpproxy creates its initial
mcp_config.jsonand a Docker daemon is reachable (docker inforesponds within 2 seconds). If Docker isn't available at first run, isolation stays off so stdio servers still work — you can enable it later from the Security page in the Web UI or by editing the config below.Existing installs: Your current
docker_isolation.enabledvalue is preserved on upgrade. To turn isolation on manually, set the top-level flag in~/.mcpproxy/mcp_config.json(or use the Web UI toggle):json{ "docker_isolation": { "enabled": true } }Existing connections will re-wrap themselves in containers after the next server restart; new connections pick up isolation immediately.
Docker isolation automatically wraps stdio-based MCP servers in Docker containers, providing:
Add to your ~/.mcpproxy/mcp_config.json:
{
"docker_isolation": {
"enabled": true,
"memory_limit": "512m",
"cpu_limit": "1.0",
"timeout": "60s",
"network_mode": "bridge",
"registry": "docker.io",
"default_images": {
"python": "python:3.11",
"python3": "python:3.11",
"uvx": "python:3.11",
"pip": "python:3.11",
"pipx": "python:3.11",
"node": "node:20",
"npm": "node:20",
"npx": "node:20",
"yarn": "node:20",
"go": "golang:1.21-alpine",
"cargo": "rust:1.75-slim",
"rustc": "rust:1.75-slim",
"ruby": "ruby:3.2-alpine",
"gem": "ruby:3.2-alpine",
"php": "php:8.2-cli-alpine",
"composer": "php:8.2-cli-alpine",
"binary": "alpine:3.18",
"sh": "alpine:3.18",
"bash": "alpine:3.18"
},
"extra_args": []
}
}
| Field | Description | Default |
|---|---|---|
enabled | Enable Docker isolation globally | false |
memory_limit | Memory limit per container | "512m" |
cpu_limit | CPU limit per container | "1.0" |
timeout | Container startup timeout | "30s" |
network_mode | Docker network mode | "bridge" |
registry | Docker registry to use | "docker.io" |
default_images | Runtime to image mappings | See above |
extra_args | Additional docker run arguments | [] |
You can override isolation settings per server:
{
"mcpServers": [
{
"name": "custom-python-server",
"command": "python",
"args": ["-m", "my_server"],
"isolation": {
"enabled": true,
"image": "my-custom-python:latest",
"network_mode": "none",
"working_dir": "/app",
"extra_args": ["--cap-drop=ALL"]
},
"enabled": true
},
{
"name": "no-isolation-server",
"command": "python",
"args": ["-m", "trusted_server"],
"isolation": {
"enabled": false
},
"enabled": true
}
]
}
Per-server isolation.enabled: true only takes effect when the global docker_isolation.enabled flag is also true. If the global flag is false, MCPProxy runs the server on the host even if you explicitly opted it into isolation in its per-server config.
Starting in this release, MCPProxy emits a one-time warning in the main log when it detects this configuration (look for per-server docker isolation opt-in ignored in ~/.mcpproxy/logs/main.log). To actually isolate those servers, flip the global flag on.
When anonymous telemetry is enabled, MCPProxy reports two Docker-related counters at daily cadence:
server_docker_available_bool — whether Docker is actually invocable. Reported true only when the docker CLI is resolvable to an absolute path and docker info --format {{.ServerVersion}} succeeds (it does not fall back to a bare docker PATH probe, which could misreport availability when the binary is only inside the macOS app bundle — see issue #696). Cached for up to 15 minutes (5 minutes when the previous probe failed, so a late Docker-Desktop launch is picked up promptly).server_docker_isolated_count — how many of your configured stdio servers are configured for isolation, i.e. servers for which ShouldIsolate() returns true. This is a configuration metric, not a count of running containers; it goes to zero whenever the global flag is off regardless of per-server opt-ins.MCPProxy automatically detects the runtime type based on the command:
python, python3 → python:3.11uvx → python:3.11 (includes uv package manager)pip, pipx → python:3.11node → node:20npm, npx → node:20yarn → node:20go → golang:1.21-alpinecargo, rustc → rust:1.75-slimruby, gem → ruby:3.2-alpinephp, composer → php:8.2-cli-alpinesh, bash → alpine:3.18alpine:3.18MCPProxy uses full Docker images (python:3.11 instead of python:3.11-slim) because:
git+https:// URLsThis trade-off prioritizes compatibility over image size.
Environment variables from server configuration are automatically passed to containers:
{
"mcpServers": [
{
"name": "api-server",
"command": "uvx",
"args": ["some-package"],
"env": {
"API_KEY": "your-secret-key",
"DEBUG": "true"
},
"enabled": true
}
]
}
These become Docker arguments: -e API_KEY=your-secret-key -e DEBUG=true
MCPProxy automatically skips isolation for servers that are already Docker commands:
{
"mcpServers": [
{
"name": "existing-docker-server",
"command": "docker",
"args": ["run", "-i", "--rm", "mcp/some-server"],
"enabled": true
// Isolation automatically skipped
}
]
}
This prevents Docker-in-Docker complications.
# Run with debug logging
mcpproxy serve --log-level=debug --tray=false
# Filter for isolation messages
mcpproxy serve --log-level=debug 2>&1 | grep -i "docker isolation"
# List MCPProxy containers
docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}"
# View logs from a specific container
docker logs <container-id>
# Watch container resource usage
docker stats
Container startup timeouts:
timeout in docker_isolation configEnvironment variables not working:
env sectionGit/package installation failures:
python:3.11 not python:3.11-slim)command not found: docker on macOS (Docker Desktop installed):
docker CLI only inside
the app bundle at /Applications/Docker.app/Contents/Resources/bin/docker —
it is not on a standard PATH dir unless you ran the optional,
admin-gated "install CLI tools" step. When mcpproxy is launched from a
LaunchAgent / tray, the captured login-shell PATH may omit this directory.docker binary to its absolute path and then
exec's it directly (no login-shell wrap) when spawning a Docker upstream —
both servers that mcpproxy isolates into docker run (uvx/npx) and upstreams
whose config command is docker (a user-supplied docker run …) — so
the spawn bypasses PATH entirely and works even without the CLI-tools step.
(The enhanced spawn PATH still includes the bundle bin dir as a
belt-and-suspenders measure.) Earlier builds resolved the absolute path but
still routed the spawn through $SHELL -l -c "<docker> run …", where the
login shell re-derived PATH from rc files and could drop the bundle dir —
so the error persisted; direct exec fixes that. Direct exec is used only when
(a) the resolved value is a verified absolute executable and (b) the docker
daemon-config env is guaranteed without the login shell — on macOS via the
startup login-shell hydration, or on any platform when DOCKER_HOST /
DOCKER_CONTEXT are already exported into mcpproxy's environment. A
non-absolute result (e.g. a shell function/alias from command -v docker), or
a rootless/remote daemon on Linux whose DOCKER_HOST lives only in the
login-shell rc, falls back to the $SHELL -l wrap (still using the resolved
absolute path when one was found) so docker run keeps inheriting the daemon
config. If you still see this error, confirm the binary exists at the bundle
path above, or run Docker Desktop's "install CLI tools".upstream_servers list reports docker_status.docker_path (the resolved
binary) and reports docker_status.available / per-server docker_available
as true only when the CLI is actually resolvable and docker info
succeeds. A false value with docker_path: "" means the CLI could not be
resolved on the spawn path.error getting credentials … docker-credential-desktop … not found in $PATH on macOS (image not yet cached):
~/.docker/config.json sets "credsStore": "desktop",
so docker shells out to docker-credential-desktop for every registry
operation — even an anonymous pull of a public image. That helper lives in the
same bundle dir as the docker CLI
(/Applications/Docker.app/Contents/Resources/bin/), which mcpproxy's
sanitized spawn PATH omits. When the isolation image isn't cached locally,
the pull invokes the helper and fails; a pre-pulled image sidesteps it because
docker run then performs no registry op (which is why direct-exec alone
looked complete on cached images — issue #715 / MCP-2877).PATH whenever docker resolves to an absolute path, so the spawned docker
can exec its sibling tooling (docker-credential-*, docker-compose,
docker-buildx) exactly as it would from a normal Docker Desktop shell. This
is applied on every docker spawn path (isolated uvx/npx servers and
user-supplied docker run … upstreams) and is a no-op when docker did not
resolve to an absolute path. If you still see this error, confirm the helper
exists at the bundle path above, or pre-pull the image with
docker pull <image>.Docker isolation provides strong security boundaries but consider:
For maximum security, consider:
"network_mode": "none" for servers that don't need network access--cap-drop=ALL to extra_args to remove Linux capabilities