Docker MCP Gateway: run MCP servers in sandboxed containers
Look at the MCP config of someone who runs Claude Desktop, Cursor, and VS Code side by side. The same four servers are defined three times. Each one launches with npx, each one reads an API key from a plaintext JSON file, and each one runs as a child process with the same filesystem access as the editor that spawned it. Change a server version and you edit three files. Rotate a key and you repeat the same edit.
Docker MCP Gateway moves that arrangement into Docker. Servers become OCI images, each one runs in its own container with capped CPU and memory, secrets come from the OS keychain, and every client on the machine talks to one gateway process instead of N servers.
Prerequisites
- Docker Desktop 4.62 or later for the
docker mcpCLI commands, or Docker Engine with one extra install step (Step 1 below) - A running Docker engine
- An MCP client: Claude Desktop, VS Code, Cursor, or anything that speaks stdio or Streamable HTTP
- No Node.js install needed. That is part of the point.
Step 1: get the CLI plugin
Check what you have:
docker mcp version
If that answers with "docker: 'mcp' is not a docker command", either your Docker Desktop is older than 4.62 or you are on plain Docker Engine. On Engine, the gateway ships as a separate binary that Docker picks up as a CLI plugin:
# download the latest release binary from github.com/docker/mcp-gateway/releases
chmod +x docker-mcp
mkdir -p ~/.docker/cli-plugins
mv docker-mcp ~/.docker/cli-plugins/docker-mcp
docker mcp version
That path is the same on Linux and macOS. On Windows the plugin lives in %USERPROFILE%\.docker\cli-plugins.
Step 2: browse the catalog
The gateway pulls server definitions from an OCI catalog. List what is in the official one:
docker mcp catalog server ls mcp/docker-mcp-catalog
Each line gives you a server name such as playwright or github-official. That name is the server ID you use in the next command. In Docker Desktop you can find the same value under MCP Toolkit > Catalog > Server ID.
Step 3: create a profile and add servers
A profile is a named set of servers and their config. It is what the gateway loads when it starts, and it is what makes "my web-dev tools" a thing you can switch on.
docker mcp profile create --name web-dev
docker mcp profile server add web-dev \
--server catalog://mcp/docker-mcp-catalog/github-official \
--server catalog://mcp/docker-mcp-catalog/playwright
A server reference can point at four kinds of source:
| URI form | Source |
|---|---|
catalog://<catalog>/<server-id> |
An OCI catalog, the usual case |
docker://<image>:<tag> |
Any Docker image |
https://<registry>/v0/servers/<id> |
The MCP community registry |
file://<path> |
A local YAML or JSON definition |
The file form is how you wrap something nobody has published yet. It needs a name, an image, and a description:
name: my-server
title: My Server
type: server
image: myimage:latest
description: Description of the server
docker mcp profile server add web-dev --server file://./my-server.yaml
docker mcp profile show web-dev
Step 4: set config values and secrets
Servers that need a token or an endpoint take it through profile config:
docker mcp profile config web-dev --set github-official.personal_access_token=<token>
docker mcp profile config web-dev --get-all
Keep in mind where that value lands. For anything sensitive, use the secret store instead, which keeps the value in the OS keychain rather than in the profile file:
docker mcp secret --help
Servers that need OAuth (GitHub's official server, for one) get authorized in Docker Desktop after you add them, under MCP Toolkit. There is no terminal prompt for the consent step.
Step 5: run the gateway
For a single local client, stdio is enough. The client spawns the gateway and the gateway spawns the server containers on demand:
docker mcp gateway run --profile web-dev
That answers the lifecycle question: nothing starts until a tool call arrives. The gateway identifies which server owns the tool, starts its container, injects the credentials, and forwards the request. Watch it happen in a second terminal:
docker ps
For several clients at once, or for a headless box where you want one long-lived endpoint, switch to a network transport:
docker mcp gateway run --profile web-dev --port 8080 --transport streaming
--transport accepts stdio, sse, or streaming. Leave out --port and it stays on stdio.
Useful flags once the basics work:
--servers server1,server2with--tools server1:* --tools server2:tool2narrows the tool list to an allowlist. Fewer tools means fewer descriptions in the model's context.--secrets=docker-desktop:./.envadds a fallback secret source after the keychain.--verbose --log-callsprints call traces, which is what you want when a tool call returns nothing useful.--dry-runstarts the gateway without listening, so you can validate a profile before wiring a client to it.--watchreloads the config when files change, handy while iterating on a profile.
Two safety defaults are already on: --block-secrets keeps secrets from being sent to tools, and --verify-signatures checks image signatures. Container limits default to 1 CPU and 2 GB per server, and both are adjustable with --cpus and --memory.
Step 6: point your clients at it
Claude Desktop reads claude_desktop_config.json. One entry replaces every server you had there:
{
"mcpServers": {
"MCP_DOCKER": {
"command": "docker",
"args": ["mcp", "gateway", "run", "--profile", "web-dev"]
}
}
}
For clients Docker knows about, let the CLI write the file:
docker mcp client connect vscode --profile web-dev
That drops a .vscode/mcp.json in the current directory, which is exactly as user-specific as it sounds:
echo ".vscode/mcp.json" >> .gitignore
Step 7: run it without Docker Desktop
On a server or in CI you often have no Desktop at all. The gateway is a container image too:
services:
gateway:
image: docker/mcp-gateway
command:
- --servers=duckduckgo
volumes:
- /var/run/docker.sock:/var/run/docker.sock
docker compose up
Note what that mount grants. The Unix socket is root-equivalent on the host, so the gateway can start sibling containers. Run this on machines you control, not on a shared runner you do not.
When to use it, and when not to
| Setup | Isolation | Config points | Fits when |
|---|---|---|---|
| Servers via npx in each client config | None, same user as your editor | One per client | One client, two servers, trying things out |
| Docker MCP Gateway | One container per server, CPU and memory caps, keychain secrets | One | Several clients, several servers, servers you did not write |
| Hosted remote MCP server with OAuth | Runs on the vendor's side | One per client plus a consent flow | Vendor-managed integrations, no local runtime desired |
The honest tradeoff: the gateway adds a dependency (Docker) and a build step for anything not already in the catalog. If you run a single MCP server in a single client, the plain stdio config is lighter and you should keep it.
Troubleshooting
docker: 'mcp' is not a docker command: Docker Desktop older than 4.62, or the plugin binary is not in~/.docker/cli-plugins. Check withdocker infofor the plugin directory Docker actually scans.- Gateway starts but no server container appears: nothing is wrong. Containers are created on the first tool call. Confirm with
docker mcp gateway run --verboseand a real prompt. - The model ignores your tools: the gateway may be exposing more than it should. Use
--toolsto cut the list to what the task needs. - A server cannot reach its API: check the profile config value first, then the
--block-networkand--block-secretsdefaults. - Client shows zero tools: run
docker mcp profile show web-devand confirm the servers are attached to the profile you passed with--profile.
Next steps
Add a --tools allowlist to your most-used profile, then compare how many tool definitions your client sends before and after. If you want per-user access control on a remote server rather than a local sandbox, that is a different problem, and it belongs to the OAuth layer of the MCP spec, not to the gateway.