How to Run Multiple MCP Servers Securely with Docker MCP Gateway and Isolated Profiles
Running third-party MCP packages directly on your laptop or server with npx or uvx is risky; it can easily leak secrets or give an AI agent more access than intended. A proper Docker MCP Gateway setup fixes this by running each MCP server in its own container, behind one secure endpoint, with limited tools and secrets.
This guide shows you a complete Docker MCP Gateway setup. If you’re planning to host this on a VPS rather than a personal machine, an affordable Linux VPS gives you the isolation and control this setup needs.
Table of Contents
Why a Direct npx/uvx Workflow Is Risky
Most MCP quick-start guides tell you to run something like npx -y @some/mcp-server directly from your AI client’s config file. Every time the client starts, that command downloads and runs code from npm or PyPI directly on your machine with full access to your files and network.
Docker’s security research found command injection flaws in many published MCP servers, and real CVEs have involved MCP services left exposed with no authentication. A solid Docker MCP Gateway setup avoids all of this by running servers in containers instead of on your host. The gateway handles every tool call, so you get logging, secret protection, and network control built in.
What Docker MCP Gateway Actually Does
Docker MCP Gateway is an open-source proxy that sits between your AI client and a set of containerized MCP servers. Instead of configuring ten different tools inside every client you use, you configure the gateway once, group servers into named profiles, and point every client at the gateway.
How a Docker MCP Gateway setup works:
- Each MCP server runs in its own container, with limited privileges, network access, and resources.
- The gateway starts a server only when a client needs it, then stops it again.
- Secrets are passed in as environment variables, never shared between containers.
- Profiles control which servers or tools a specific client can see and use.
- Every tool call is logged, so you have a record of what ran and when.
Prerequisites for Docker MCP Gateway Setup
Before starting, confirm the host is ready. This tutorial assumes you have a Linux server running Ubuntu 22.04 or newer.
- Docker engine is installed.
- Your user is added to the
dockergroup so you don’t need sudo for every command. - At least 2 vCPUs and 4 GB RAM as a starting point, and more if you plan to run several MCP servers concurrently.
- SSH access with key-based authentication.
Check that Docker is installed and your user has access:
docker --version
docker compose version
groups $USER | grep docker
If docker isn’t in your groups list, add it and re-login:
sudo usermod -aG docker $USER
newgrp docker
Step 1. Install the docker-mcp CLI Plugin
Docker Desktop bundles the MCP Toolkit automatically, but on a Linux server, you can install the docker-mcp CLI plugin manually as part of the Docker MCP Gateway setup.
Download the latest binary from the official releases:
mkdir -p ~/.docker/cli-plugins
curl -fsSL \
https://github.com/docker/mcp-gateway/releases/download/v0.43.3/docker-mcp-linux-amd64.tar.gz \
| tar -xz -C ~/.docker/cli-plugins/
chmod +x ~/.docker/cli-plugins/docker-mcp
Verify the installation:
docker mcp --version
Output
v0.43.3
At this point, the plugin is registered correctly, and this stage of the Docker MCP Gateway setup is done.
Step 2. Fix the docker-pass Dependency on Linux
The docker mcp CLI expects a docker-pass plugin for secret storage, which ships with Docker Desktop on macOS but not on Linux. Without it, commands like docker mcp server ls fail with docker pass has not been installed.
Install the credential helper and a small wrapper script:
sudo mkdir -p /usr/local/lib/docker/cli-plugins
curl -fsSL \
https://github.com/docker/docker-credential-helpers/releases/download/v0.9.8/docker-credential-pass-v0.9.8.linux-amd64 \
-o /usr/local/bin/docker-credential-pass
sudo tee /usr/local/lib/docker/cli-plugins/docker-pass > /dev/null << 'EOF'
#!/bin/bash
if [[ "$1" == "docker-cli-plugin-metadata" ]]; then
echo '{"SchemaVersion":"0.1.0","Vendor":"Docker","Version":"v1.0.0","ShortDescription":"Docker Pass secrets helper"}'
exit 0
fi
exec docker-credential-pass "$@"
EOF
sudo chmod +x /usr/local/lib/docker/cli-plugins/docker-pass
docker-credential-pass version
Note: You don’t need this if you’re using the --secrets file method later, but it stops CLI warnings and lets commands like docker mcp server ls run cleanly.
Step 3. Design Isolated Profiles Before You Add Servers
A profile is a named collection of MCP servers and their configuration. Instead of one big pool of tools every client can see, profiles let you scope exposure to exactly what each use case needs. This is the isolation piece of a proper Docker MCP Gateway setup.
A simple starting setup:
dev-profile: Filesystem, Git, and a code search server, scoped to a single project directory.research-profile: A web-search or fetch server, no filesystem access at all.ops-profile: Infrastructure tools with read-only mounts.
Keeping these separate means a compromised or misbehaving tool in research-profile never gets anywhere near your filesystem or infrastructure credentials. Treat each profile in your Docker MCP Gateway setup as its own trust boundary, not just an organizational label.
Step 4. Build the Server Catalog
The gateway reads server definitions from catalog files. Create a working directory and a registry listing which servers are enabled:
mkdir -p ~/.docker/mcp/catalogs
mkdir -p ~/.docker/mcp/secrets
cat > ~/.docker/mcp/registry.yaml << 'EOF'
registry:
filesystem:
ref: ""
git:
ref: ""
fetch:
ref: ""
EOF
Point the gateway at both the official Docker MCP Catalog and your own custom catalog:
cat > ~/.docker/mcp/catalog.json << 'EOF'
{
"catalogs": {
"docker-mcp": {
"displayName": "Docker MCP Catalog",
"url": "https://desktop.docker.com/mcp/catalog/v2/catalog.yaml"
},
"custom-catalog": {
"displayName": "Custom Servers",
"url": "/home/youruser/.docker/mcp/catalogs/custom-catalog.yaml"
}
}
}
EOF
For any custom or private MCP server image, define it explicitly, including every environment variable it needs under secrets:
cat > ~/.docker/mcp/catalogs/custom-catalog.yaml << 'EOF'
registry:
internal-api:
title: Internal API Server
description: Wraps an internal REST API as MCP tools
image: yourregistry/internal-mcp:latest
type: server
tools: []
secrets:
- name: internal-api.token
env: API_TOKEN
description: Bearer token for the internal API
EOF
Pull the standard catalog images you plan to use:
docker pull mcp/filesystem:latest
docker pull mcp/git:latest
docker pull mcp/fetch:latest
Step 5. Isolate Secrets Per Server
On Linux, the Docker Desktop keychain-based secrets engine isn’t available, so this Docker MCP Gateway setup uses a permission-locked secrets file instead. This keeps credentials out of individual container images and out of your shell history.
cat > ~/.docker/mcp/secrets.env << 'EOF'
internal-api.token=replace-with-real-token
git.ssh_key_path=/home/youruser/.ssh/mcp_deploy_key
EOF
chmod 600 ~/.docker/mcp/secrets.env
Two things to check here:
- The secrets file itself never goes into any container; the gateway reads it once and only passes each server the variables listed in its own catalog entry.
- If a server’s catalog entry doesn’t list a secret, that server never gets it, even if another server running alongside it does.
You can confirm this behavior with a dry run before going live, covered in the next step.
Step 6. Dry-Run and Launch the Gateway
Always check your setup before going live. A dry run shows which containers would start and which secrets each one would get without starting anything:
docker mcp gateway run \
--dry-run \
--verbose \
--secrets ~/.docker/mcp/secrets.env
Check the output for:
Warning: Secret '...' not found
This means a name mismatch between secrets.env and your catalog file. Fix any warnings before continuing this Docker MCP Gateway setup.
Once the dry run is clean, start the gateway for real over HTTP/SSE so remote clients can reach it:
docker mcp gateway run \
--transport sse \
--port 8811 \
--secrets ~/.docker/mcp/secrets.env
For local, single-client use, run it over stdio instead; it’s the default and doesn’t need a port:
docker mcp gateway run --profile dev-profile
Step 7. Run the Gateway as a systemd Service
For a production Docker MCP Gateway setup on a VPS, you want the gateway to survive reboots and crashes automatically rather than running it in a terminal session. To do this, use:
sudo tee /etc/systemd/system/mcp-gateway.service > /dev/null << EOF
[Unit]
Description=Docker MCP Gateway
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target
[Service]
Type=simple
User=$(whoami)
Environment=HOME=$HOME
ExecStart=/usr/bin/docker mcp gateway run \
--transport sse \
--port 8811 \
--secrets $HOME/.docker/mcp/secrets.env
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
Generate a stable auth token so clients don’t need reconfiguring after every restart:
sudo mkdir -p /etc/systemd/system/mcp-gateway.service.d
TOKEN=$(openssl rand -hex 32)
echo "Save this token securely: $TOKEN"
sudo tee /etc/systemd/system/mcp-gateway.service.d/token.conf > /dev/null << EOF
[Service]
Environment=MCP_GATEWAY_AUTH_TOKEN=$TOKEN
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now mcp-gateway.service
sudo systemctl status mcp-gateway.service
Safe Docker Compose Configuration for MCP Gateway
The CLI approach above is the most flexible way to run the gateway on Docker Engine, but you can also describe the same Docker MCP Gateway setup in a Compose file. This is useful when you want the gateway and its dependencies under version control together.
mkdir mcp-gateway && cd mcp-gateway
cat > docker-compose.yml << 'EOF'
services:
mcp-gateway:
image: docker/mcp-gateway:latest
container_name: mcp-gateway
restart: unless-stopped
command:
- "gateway"
- "run"
- "--transport=sse"
- "--port=8811"
- "--secrets=/run/secrets/mcp_secrets"
ports:
- "127.0.0.1:8811:8811"
volumes:
- ./catalog.yaml:/etc/mcp/catalog.yaml:ro
- ./registry.yaml:/etc/mcp/registry.yaml:ro
secrets:
- mcp_secrets
read_only: true
tmpfs:
- /tmp
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
networks:
- mcp_internal
mcp-filesystem:
image: mcp/filesystem:latest
restart: unless-stopped
volumes:
- ./workspace:/workspace:ro
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
networks:
- mcp_internal
deploy:
resources:
limits:
cpus: "0.5"
memory: 256M
secrets:
mcp_secrets:
file: ./secrets.env
networks:
mcp_internal:
driver: bridge
internal: true
EOF
Key security-safe choices built into this Docker MCP Gateway setup:
- The gateway listens only on
127.0.0.1, so you reach it via a reverse proxy or SSH tunnel, not directly from the internet. - The
mcp_internalnetwork isinternal: true, so MCP containers can’t talk to the public internet unless you route them through the gateway. - Filesystem mounts use
:ro(read-only) wherever a server doesn’t need write access. cap_drop: ALLandno-new-privilegesremove extra Linux capabilities and block privilege escalation for each container.- CPU and memory limits make sure one bad tool can’t consume all resources on the host.
Bring it up and check container status:
docker compose up -d
docker compose ps
docker compose logs -f mcp-gateway
Review MCP Tools Before Use
After launch, always confirm which tools the gateway is actually exposing before connecting any client. This is a critical verification step in any Docker MCP Gateway setup:
docker mcp server ls
docker mcp tools list --profile dev-profile
Compare the command output with what you expect from your registry and catalog files. If you see a tool you never added, you probably pulled in an extra catalog by mistake or misconfigured a profile so its boundary is too wide.
Link AI Apps to MCP Profiles
Most MCP-aware clients, including Claude Desktop, Cursor, Continue, n8n, and Windsurf, can connect either over stdio or to a remote SSE/HTTP endpoint. For a remote Docker MCP Gateway setup, use mcp-remote as a small bridge between the client and the gateway:
{
"mcpServers": {
"gateway": {
"command": "npx",
"args": [
"mcp-remote",
"http://your-server:8811/sse",
"--header",
"Authorization: Bearer YOUR_TOKEN_HERE",
"--allow-http",
"--transport",
"sse-only"
]
}
}
}
Two flags are important here: --allow-http because mcp-remote blocks HTTP by default, and --transport sse-only to avoid the sessionid must be provided error when the client tries Streamable HTTP first. Once connected, the client should only show the tools from the profile you chose, proving that this Docker MCP Gateway setup keeps tools isolated as intended.
Conclusion
Running MCP servers directly on your host with npx or uvx is easy, but it seriously increases your security risk. A Docker MCP Gateway setup with isolated profiles, per-server secrets, read-only mounts, and dropped capabilities gives you the same tools with less danger, and proper logs you can review.
Once the gateway, catalog, and Compose file are in place, adding a new MCP server becomes a quick change instead of a new attack surface on your main machine.
Use isolated MCP profiles on a PerLod Linux VPS instead of exposing your development machines to third-party tools.
We hope you enjoy this guide.
For automated deployment workflows once your gateway stack is running, you can check how to install Dokploy on a Linux VPS to manage Compose-based services through a UI.
FAQs
What’s the difference between a profile and a catalog in Docker MCP Gateway?
A catalog defines available servers; a profile is a named subset of those servers exposed to a specific client or use case.
Can one MCP server read another server’s secrets?
No. The gateway only injects the secrets listed in each server’s own catalog entry.
Do I need Docker Desktop for Docker MCP Gateway Setup?
No. This guide uses Docker Engine on a Linux server; the CLI plugin is installed manually.