MCP Galaxy / Good to Know / MCP Gateway
★ Guide Docker MCP Reference Docker Desktop

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.

Target
Docker Desktop
Source
docker/mcp-gateway
Scope
Reference
01, Overview

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.

Our deployments target Docker Desktop. This guide is built around that path. The Docker Engine section near the end is included so the moving parts are understandable on a headless Linux host, it is not a deployment recommendation.
02, Configuration

The Three Configuration Files

The gateway is driven by three artifacts living under ~/.docker/mcp/:

A
Catalog
catalogs/custom.yaml, defines servers that exist: image, secrets needed, description.
B
Registry
registry.yaml, defines servers that are enabled: which catalog entries to load.
C
Secrets
Provides values for the secrets each enabled server needs. Source differs between Desktop and Engine.

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:

~/.docker/mcp/catalogs/custom.yaml
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:

Multiple servers in one catalog
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:

Gateway args, multiple catalogs
--catalog=/mcp/catalogs/custom.yaml
--catalog=/mcp/catalogs/docker-mcp.yaml
Caveat. 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:

~/.docker/mcp/registry.yaml
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.

03, Primary Path

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.

Step 1, Place the catalog and registry
Copy the catalog into place
$ 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.

Step 2, Set secrets

Stored in Docker Desktop's secret store, set once with the CLI:

docker mcp secret set
$ 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.

Step 3, Point the client at the gateway

The client (Claude Desktop, Claude Code, Codex, etc.) gets pointed at the gateway with three bind-mounts:

claude_desktop_config.json
{
  "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:

MountPurpose
/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.

Same args, every client. The same gateway invocation works for Claude Desktop's 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.
04, Reference Path

Docker Engine (Headless Linux)

Not a supported deployment target for us. This section exists so you can recognize the architecture and failure modes if you encounter an Engine-only host.

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.

1, Install the docker-mcp CLI plugin manually
Install the plugin
$ 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
2, Use a secrets.env file, not the Desktop secret daemon

The 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:

~/.docker/mcp/secrets.env
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.

3, Adjust the client config block

Drop the secret-engine bind-mount and add --secrets=:

claude_desktop_config.json (Engine variant)
{
  "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>.

Bypassing the gateway entirely

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.

05, Quick Reference

Desktop vs Engine

ConcernDocker 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.

06, Gotchas

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.
A new server in the catalog isn't showing up. Did you also add it to registry.yaml with enabled: true? Catalog defines existence; registry defines activation. Both are required.
Authentication errors after restarting the firewall or target system. The catalog's 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.
07, Architecture

At a Glance

Data flow
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.