Docker MCP Gateway
How the Docker MCP Gateway connects MCP clients, Claude Desktop, Claude Code, Codex, Cursor, Gemini, and friends, to MCP servers, and how the same setup differs between Docker Desktop and plain Docker Engine.
What the MCP Gateway Is
The MCP Gateway is an open-source Docker project
(docker/mcp-gateway)
that acts as a single connection point between MCP clients and MCP servers.
Without the gateway, every client needs its own configuration for every server, X clients × Y servers separate configuration entries. With the gateway, each client points at one place; the gateway handles everything behind it.
Concretely, the gateway:
- Reads a catalog of available servers.
- Reads a registry of which servers are enabled.
- Resolves secrets from a secret source.
- Starts each enabled server as a Docker container on demand.
- Routes MCP tool calls from clients to the right server container.
- Tears down server containers when they're no longer needed.
Each server runs in its own isolated container with restricted privileges, network access, and resource limits.
The Three Configuration Files
The gateway is driven by three artifacts living under ~/.docker/mcp/:
catalogs/custom.yaml, defines servers that exist: image, secrets needed, description.registry.yaml, defines servers that are enabled: which catalog entries to load.The split matters: you can have many servers defined in a catalog and toggle which ones are active without touching the catalog itself.
Catalog (custom.yaml). Top level is version: "2" and a registry: map. Each key under registry: is a server, with metadata and a secrets: list naming the env vars the container needs:
version: "2"
registry:
paloalto-firewall:
name: Palo Alto Firewall
description: Read and write access to a PAN-OS firewall via the XML API.
categories: [security, network]
image: paloalto-mcp-server:latest
secrets:
- name: PANOS_HOST
env: PANOS_HOST
required: true
description: Hostname or IP of the firewall management interface
- name: PANOS_API_KEY
env: PANOS_API_KEY
required: true
description: PAN-OS API key
Multiple servers in one catalog. A catalog is just a YAML map under registry:. Add as many sibling entries as you want:
version: "2"
registry:
paloalto-firewall:
name: Palo Alto Firewall
image: paloalto-mcp-server:latest
secrets: [...]
cisco-asa:
name: Cisco ASA
image: cisco-asa-mcp-server:latest
secrets: [...]
internal-ticketing:
name: Internal Ticketing
image: ghcr.io/acme/ticketing-mcp:1.4.0
secrets: [...]
Multiple catalogs. You can also keep separate catalog files and pass multiple --catalog= flags to the gateway. This is useful for layering an in-house catalog on top of Docker's official one:
--catalog=/mcp/catalogs/custom.yaml --catalog=/mcp/catalogs/docker-mcp.yaml
docker mcp catalog import is known to overwrite
rather than merge entries (tracked in
docker/mcp-gateway#126).
Editing the YAML by hand and cp-ing it into place sidesteps the issue.
Registry (registry.yaml). Same registry: top-level key, but only listing the servers you want active and which catalog they come from:
registry:
paloalto-firewall:
catalog: custom
enabled: true
cisco-asa:
catalog: custom
enabled: true
internal-ticketing:
catalog: custom
enabled: false # staged but turned off
Secrets. This is where Docker Desktop and Docker Engine diverge most. See the two deployment sections below.
Docker Desktop Deployment
Docker Desktop bundles everything: the docker mcp CLI plugin, the secret store daemon, and ready access to the gateway image. The gateway runs as a container the client invokes per session.
$ mkdir -p ~/.docker/mcp/catalogs $ cp custom-catalog.yaml ~/.docker/mcp/catalogs/custom.yaml
Then edit ~/.docker/mcp/registry.yaml to enable the server entry under the shared registry: key.
Stored in Docker Desktop's secret store, set once with the CLI:
$ docker mcp secret set PANOS_HOST="firewall.example.com" $ docker mcp secret set PANOS_API_KEY="LUFRPT1xxxxxxxxxxxxxxxxxxxxxxxxxx==" $ docker mcp secret set PANOS_VERIFY_SSL="yes" $ docker mcp secret list
These values aren't stored in any file you commit; the gateway reads them at runtime through a Unix socket Desktop exposes.
The client (Claude Desktop, Claude Code, Codex, etc.) gets pointed at the gateway with three bind-mounts:
{
"mcpServers": {
"mcp-toolkit-gateway": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"-v", "/var/run/docker.sock:/var/run/docker.sock",
"-v", "/Users/<your-username>/.docker/mcp:/mcp",
"-v", "/Users/<your-username>/Library/Caches/docker-secrets-engine/engine.sock:/root/.cache/docker-secrets-engine/engine.sock",
"docker/mcp-gateway:latest",
"--catalog=/mcp/catalogs/custom.yaml",
"--registry=/mcp/registry.yaml",
"--transport=stdio"
]
}
}
}
What each bind-mount does:
| Mount | Purpose |
|---|---|
/var/run/docker.sock |
Lets the gateway spawn server containers. |
~/.docker/mcp |
Lets the gateway read your catalog and registry. |
docker-secrets-engine/engine.sock |
Lets the gateway resolve secret values from Desktop's secret store. |
Drop any one of these and the gateway breaks: no Docker socket means no container spawning; no mcp/ mount means no catalog or registry; no secret socket means empty env values and docker run -e "" rejects the env flags, so server containers never start and only the gateway's own admin tools show up.
claude_desktop_config.json, Claude Code's
~/.claude.json or .mcp.json, and Codex's
.codex/config.toml (in TOML form). Only the surrounding file format
changes.
Docker Engine (Headless Linux)
The conceptual model is identical to Desktop, catalog, registry, gateway spawns containers, but Desktop does several things automatically that you'd have to do yourself on Engine.
docker-mcp CLI plugin manually$ sudo mkdir -p /usr/local/lib/docker/cli-plugins $ sudo curl -fsSL \ https://github.com/docker/mcp-gateway/releases/latest/download/docker-mcp-linux-amd64.tar.gz \ | sudo tar -xz -C /usr/local/lib/docker/cli-plugins/ $ sudo chmod +x /usr/local/lib/docker/cli-plugins/docker-mcp $ docker mcp --version
secrets.env file, not the Desktop secret daemonThe Desktop secret store doesn't exist on Engine. Calls like docker mcp secret set … fail with dial unix /root/.docker/desktop/jfs.sock: connect: no such file or directory because that socket is a Desktop-only component.
Use a plain key=value file instead:
PANOS_HOST=firewall.example.com PANOS_API_KEY=LUFRPT1xxxxxxxxxxxxxxxxxxxxxxxxxx== PANOS_VERIFY_SSL=yes PANOS_VSYS=vsys1 PANOS_ENABLE_WRITE=yes
Save the file and chmod 600 it. The catalog's secrets: blocks stay exactly as they are, the gateway just resolves the names from this file instead of the daemon.
Drop the secret-engine bind-mount and add --secrets=:
{
"mcpServers": {
"mcp-toolkit-gateway": {
"command": "docker",
"args": [
"run", "-i", "--rm",
"-v", "/var/run/docker.sock:/var/run/docker.sock",
"-v", "/home/<your-username>/.docker/mcp:/mcp",
"docker/mcp-gateway:latest",
"--catalog=/mcp/catalogs/custom.yaml",
"--registry=/mcp/registry.yaml",
"--secrets=/mcp/secrets.env",
"--transport=stdio"
]
}
}
}
Note the home directory path is /home/<user> rather than /Users/<user>.
For a single server on an Engine box, you can skip the gateway and run the server container directly via docker run --env-file .env. You lose the multi-server multiplexing and centralized lifecycle, but for a one-off integration it's the lightest path. Useful to know when triaging whether a problem is the server or the gateway plumbing around the server.
Desktop vs Engine
| Concern | Docker Desktop (our target) | Docker Engine |
|---|---|---|
docker mcp CLI plugin |
Bundled. | Install manually from GitHub releases. |
| Gateway image | docker/mcp-gateway:latest |
docker/mcp-gateway:latest, same. |
| Catalog format | version: "2", registry: map. |
Same. |
| Registry format | registry:, per-server enabled. |
Same. |
| Secret storage | Desktop secret daemon (docker mcp secret set). |
secrets.env file. |
| Bind-mounts in client config | 3, docker.sock, ~/.docker/mcp, secret-engine .sock. |
2, docker.sock, ~/.docker/mcp. |
| Gateway flag for secrets | None, daemon is mounted. | --secrets=/mcp/secrets.env |
| Home directory prefix | /Users/<user> (macOS) |
/home/<user> (Linux) |
The catalog and registry files are byte-for-byte portable across both. Only how secrets reach the gateway changes.
Remember
docker mcp secret set fails with no such file or directory on jfs.sock.
You're on Docker Engine, not Desktop. Switch to a secrets.env file and
--secrets= on the gateway.
docker mcp catalog import wipes existing entries.
Known issue
(docker/mcp-gateway#126).
Edit the YAML by hand and cp it into place.
registry.yaml with enabled: true?
Catalog defines existence; registry defines activation. Both are required.
secrets: list points at the env vars, but the actual values
live in Desktop's secret store. If you rotated a credential, you need to re-run
docker mcp secret set, the catalog doesn't need to change.
At a Glance
MCP Client (Claude Desktop, Claude Code, Codex, Gemini …)
│
│ stdio
▼
Docker MCP Gateway ──reads──▶ catalog (what exists)
│ registry (what's enabled)
│ secrets (values, via Desktop daemon or .env)
│
│ spawns containers via docker.sock
▼
MCP Server Container ──HTTPS──▶ Target API (firewall, ticketing, etc.)
Catalogs and registries are portable. The gateway image is the same on both platforms. The only meaningful difference between Docker Desktop and Docker Engine is how secret values get from your machine into the server container's environment.