Docker Security Isolation
June 18, 2026 · View on GitHub
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):{ "docker_isolation": { "enabled": true } }Existing connections will re-wrap themselves in containers after the next server restart; new connections pick up isolation immediately.
Overview
Docker isolation automatically wraps stdio-based MCP servers in Docker containers, providing:
- Process Isolation: Each server runs in a separate container
- File System Isolation: Servers cannot access host file system
- Network Isolation: Configurable network modes for security
- Resource Limits: Memory and CPU limits prevent resource exhaustion
- Automatic Runtime Detection: Maps commands to appropriate Docker images
Configuration
Global Docker Isolation
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": []
}
}
Configuration Options
| 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 | [] |
Per-Server Configuration
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
}
]
}
Gotcha: global flag gates per-server opt-ins
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.
Telemetry
When anonymous telemetry is enabled, MCPProxy reports two Docker-related counters at daily cadence:
server_docker_available_bool— whether Docker is actually invocable. Reportedtrueonly when thedockerCLI is resolvable to an absolute path anddocker info --format {{.ServerVersion}}succeeds (it does not fall back to a baredockerPATH 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 whichShouldIsolate()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.
Runtime Detection
MCPProxy automatically detects the runtime type based on the command:
Python Environments
python,python3→python:3.11uvx→python:3.11(includes uv package manager)pip,pipx→python:3.11
Node.js Environments
node→node:20npm,npx→node:20yarn→node:20
Other Languages
go→golang:1.21-alpinecargo,rustc→rust:1.75-slimruby,gem→ruby:3.2-alpinephp,composer→php:8.2-cli-alpine
Shell/Binary
sh,bash→alpine:3.18- Unknown commands →
alpine:3.18
Why Full Images Instead of Slim/Alpine?
MCPProxy uses full Docker images (python:3.11 instead of python:3.11-slim) because:
- Git Support: Many MCP servers install packages from Git repositories using
git+https://URLs - Build Tools: Some packages require compilation during installation
- System Dependencies: Full images include common libraries needed by MCP servers
This trade-off prioritizes compatibility over image size.
Environment Variables
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
Docker-in-Docker Prevention
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.
Debugging
Check Docker Isolation Status
# 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"
Monitor Docker Containers
# 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
Common Issues
Container startup timeouts:
- Increase
timeoutin docker_isolation config - Check if Docker images need to be pulled
- Verify network connectivity for package installations
Environment variables not working:
- Check that variables are defined in server
envsection - Use debug logging to see Docker command arguments
- Verify container has access to required environment
Git/package installation failures:
- Ensure using full images (
python:3.11notpython:3.11-slim) - Check container logs for specific error messages
- Verify network access for package repositories
command not found: docker on macOS (Docker Desktop installed):
- Docker Desktop installed the default way leaves the
dockerCLI only inside the app bundle at/Applications/Docker.app/Contents/Resources/bin/docker— it is not on a standardPATHdir unless you ran the optional, admin-gated "install CLI tools" step. When mcpproxy is launched from a LaunchAgent / tray, the captured login-shellPATHmay omit this directory. - mcpproxy resolves the
dockerbinary to its absolute path and then exec's it directly (no login-shell wrap) when spawning a Docker upstream — both servers that mcpproxy isolates intodocker run(uvx/npx) and upstreams whose configcommandisdocker(a user-supplieddocker run …) — so the spawn bypassesPATHentirely and works even without the CLI-tools step. (The enhanced spawnPATHstill 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-derivedPATHfrom 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 whenDOCKER_HOST/DOCKER_CONTEXTare already exported into mcpproxy's environment. A non-absolute result (e.g. a shell function/alias fromcommand -v docker), or a rootless/remote daemon on Linux whoseDOCKER_HOSTlives only in the login-shell rc, falls back to the$SHELL -lwrap (still using the resolved absolute path when one was found) sodocker runkeeps 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 listreportsdocker_status.docker_path(the resolved binary) and reportsdocker_status.available/ per-serverdocker_availableastrueonly when the CLI is actually resolvable anddocker infosucceeds. Afalsevalue withdocker_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 Desktop's default
~/.docker/config.jsonsets"credsStore": "desktop", sodockershells out todocker-credential-desktopfor 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 spawnPATHomits. When the isolation image isn't cached locally, the pull invokes the helper and fails; a pre-pulled image sidesteps it becausedocker runthen performs no registry op (which is why direct-exec alone looked complete on cached images — issue #715 / MCP-2877). - mcpproxy now prepends the resolved docker binary's bundle dir to the child
PATHwheneverdockerresolves 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-supplieddocker run …upstreams) and is a no-op whendockerdid 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 withdocker pull <image>.
Security Considerations
Docker isolation provides strong security boundaries but consider:
- Network Access: Containers can still access the network by default
- Resource Limits: Set appropriate memory/CPU limits
- Image Trust: Use trusted base images from official repositories
- Secrets: Environment variables are visible in container inspect output
For maximum security, consider:
- Using
"network_mode": "none"for servers that don't need network access - Adding
--cap-drop=ALLto extra_args to remove Linux capabilities - Using custom minimal images for specific use cases