Running an MCP gateway in Docker Compose turns scattered, per-server access controls into one enforcement point for authentication, policy, and audit logging. Without that gateway, each MCP server manages its own access controls on its own terms, so there is no consistent policy across the environment and the audit trail has gaps wherever a server was configured loosely or not at all.Tool poisoning and prompt injection, embedding malicious instructions inside tool descriptions and metadata that the model reads but the user never sees, are classified in the OWASP MCP Top 10 (published 2025) as the dominant MCP risk categories. That architecture gets exploited in practice: a GitHub MCP server was compromised through a malicious issue that injected hidden instructions, which hijacked the agent and pulled data out of private repositories, and Check Point Research disclosed a remote code execution path in Claude Code through poisoned repository config files.
The failure mode extends past any single incident. An architectural flaw disclosed in April 2026 by OX Security exposes an estimated200,000vulnerable instances across a supply chain tied to an enormous volume of package downloads, and Trend Micro identified 492 MCP servers sitting on the open internet with no authentication at all. Only a small fraction of organizations have a formal identity strategy for AI agents, and most AI pilots stall before reaching production because identity management, auditability, and vendor lock-in never get resolved.
What a Docker Compose MCP gateway stack consists of
At the center sits the gateway container itself, typically the docker/mcp-gateway:latest image, which acts as the routing and enforcement process that proxies every piece of MCP traffic passing through the stack. Docker Compose is what ties this environment together as asingle deployable unit: one docker compose up command brings the gateway online alongside its dependent services, and those services reach each other by name without anyone having to wire up ports by hand.
Alongside the gateway sits a catalog file, usually catalog.yaml, which defines exactly which MCP servers the gateway exposes. Each entry in that file specifies the server's type, its command, its arguments, and any secrets referenced through environment variables rather than written out directly. Credentials themselves live in a separate secrets file, typically.mcp.env, written in standard environment variable format and kept apart from the Compose file so API keys, tokens, and connection strings never end up mixed into configuration that might get shared or committed.
A production stack usually adds dependent services such as a database like Postgres, a cache like Redis, and a reverse proxy like nginx or Caddy. ContextForge's Compose stack, for example, runs Gateway, Postgres, Redis, and optional MCP servers behind a single YAML file, and routes all gateway traffic through nginx while the gateway container's own port stays deliberately disabled.The hwdsl2/mcp-gateway imagetakes a related approach, putting a Caddy reverse proxy in front to enforce Bearer token authentication on every API request, along with a dashboard for monitoring server status.
One more decision belongs at this stage, before any YAML gets written: transport mode. Stdio transport is fine for local development, but any production deployment needs SSE or Streamable HTTP instead. The docker/mcp-gateway:latest image supports stdio, SSE, and streaming (Streamable HTTP) transport, switchable via a --transport flag set in the Compose command block, such as --transport=sse. The miko550 reference implementation, for instance, configures the gateway for Streamable HTTP at a server address ending in:8811/mcp. With the gateway, the catalog, the secrets file, the supporting services, and the transport mode mapped out, the next step is turning that map into an actual docker-compose.yml.
How to write the docker-compose.yml for a gateway stack
The gateway service definition itself is the simplest block to write but carries the most consequence: it pulls the docker/mcp-gateway:latest image, and its command block needs --transport=sse or --transport=streamable-http explicitly set for anything beyond a local test, since stdio has no place in production.
Port exposure deserves its own line of attention. Expose only the gateway's own port to the host, while every internal service port stays closed off. ContextForge's Compose file disables the gateway container's own port entirely and routes all traffic through nginx on port8080instead. For anything facing the open internet, bind that port to127.0.0.1 rather than 0.0.0.0. Thehwdsl2/docker-mcp-gatewaydocumentation spells this out directly, using a flag like-p 127.0.0.1:3000:3000/tcpso the unencrypted port never becomes directly reachable from outside.
gateway:Secrets are the single most commonly misconfigured element in this file. Docker Compose's native secrets block maps named secrets to files rather than environment variables, which keeps credentials out of the exposure paths that environment variables create. The.mcp.env file, written in standard environment variable format, sits next to docker-compose.yml and holds MCP credentials, but it should never be committed to source control. Docker MCP's own secret command stores credentials in Docker's local secrets store directly, rather than leaving them scattered across text files.
image: docker/mcp-gateway:latest
command: ["--transport=sse"]
ports:
- "127.0.0.1:8811:8811"
secrets:Credentials live only in the container's process environment at runtime, never baked into an image or checked into a config file anywhere in the repository.
github_token:
file:./secrets/github_token.txt agents.yaml, which sits alongside the Compose file, defines the agent configurations. catalog.yaml defines which MCP servers the gateway exposes, with entries for servers such as filesystem and github, each specifying type, command, args, and environment-variable-referenced secrets.
Health checks matter just as much as secrets, though they get far less attention. A service_healthy condition on the gateway's dependencies prevents the gateway from starting before its database is actually ready, which removes a startup race condition that otherwise shows up intermittently and is hard to reproduce.
depends_on:If a dependency goes temporarily unavailable after startup, the gateway should retry with exponential backoff and jitter rather than hammering the connection repeatedly.
postgres:
condition: service_healthy
On first start, an API key is auto-generated and displayed in container logs, and all API requests require this key.
A few working references show this pattern at different scales. Themiko550/docker-mcp-gatewayproject runs the gateway through Docker Compose with a sample custom setup and remote access, and gets running by cloning the repo, downloading the catalog, and running docker compose up -d.ajeetraina's production implementationgoes further, running a multi-agent system spanning research, analysis, and content creation across multiple MCP servers (GitHub, Brave, Wikipedia), with interceptors, stronger secrets management, and a Compose file built for monitoring and scaling. Docker's own official guide keeps things minimal: it defines the MCP Gateway service for GitHub, runs docker/mcp-gateway:latest with GitHub configured as the server, and exposes the gateway on port8080.
Configuring MCP server entries and credential injection in the catalog
The catalog file is where the gateway learns which tools exist and how to reach them, and how credentials get referenced inside it decides whether secrets stay protected at runtime or leak into logs and images. Each entry in the catalog specifies a server's type, command, arguments, and any secrets, and those secrets should always be referenced by name rather than by value, with the actual value injected from the secrets store only at runtime.
A wrong catalog entry hardcodes a token directly into the YAML, something likeenv: { GITHUB_TOKEN: "ghp_abc123..." }, for a server such as github, which requires MCP_GITHUB_TOKEN for GitHub API access to repos, issues, and PRs. A correct entry references the secret by name instead, something likeenv: { GITHUB_TOKEN: "${GITHUB_TOKEN}" }, with the real value resolved from Docker's secrets store at container start, never written anywhere in the catalog itself.
Thehwdsl2/docker-mcp-gatewaydocumentation lists the built-in servers available and what each one needs to run. The fetch server needs no configuration at all and simply fetches URLs and extracts their content. The filesystem server requires MCP_FILESYSTEM_DIRS and handles reading and writing files inside the directories it's allowed to touch. The github server requires MCP_GITHUB_TOKEN for access to repositories, issues, and pull requests through the GitHub API. The brave-search server requires MCP_BRAVE_API_KEY for web search through the Brave Search API. The git server requires MCP_GIT_REPO and exposes repository tools like status, diff, commit, and log. The postgres server requires MCP_POSTGRES_URL to query PostgreSQL databases directly. Servers get turned on by listing them, comma-separated, in MCP_SERVERS, and for the filesystem server specifically, the host directories it touches need to be bind-mounted into the container.
Docker's own CLI gives a cleaner path for setting these credentials than editing environment files by hand. The official setup runs[docker mcp secret](https://www.docker.com/blog/docker-mcp-ai-agent-developer-setup/) set github.personal_access_token=<YOUR_GITHUB_TOKEN>to store the token directly in Docker's secrets store, so it never appears in the Compose file or in any environment variable a process could accidentally print. The --block-secrets flag scans both inbound and outbound tool payloads for credential leakage, which closes off a specific vector where a tool's own output accidentally returns a secret it had no business exposing.
Beyond what Docker's native tooling covers, production environments generally need a centralized secrets management system sitting behind the gateway, something like HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault. Credential rotation should be automated on a realistic schedule starting within the first few weeks of production deployment, rather than left as a manual task someone remembers to do eventually.
Authentication at the gateway layer and MCP spec requirements
A gateway that routes traffic without verifying who's sending it reproduces the exact exposure it was built to eliminate, and the MCP specification now mandates a specific mechanism for every remote connection. That requirement didn't exist at the start. The original MCP spec shipped with no standardized authentication mechanism at all.
The March 2025 update changed that by introducing OAuth 2.1, including PKCE, as the authorization framework for remote MCP connections. The June 2025 update went further, formally classifying every MCP server as an OAuth resource server, and by the November 2025 revision, PKCE had become explicitly mandatory for every client connecting to one.
The June 2026Enterprise-Managed Authorizationextension carries this further into enterprise identity infrastructure directly. Under EMA, a user authenticates once through the enterprise identity provider already in place, and that IdP becomes the authoritative source for provisioning access to MCP servers. In practice, that means the gateway no longer has to reinvent identity verification. It checks a token issued by an identity provider the organization already trusts, scopes that identity's access according to the catalog entries it's permitted to reach, and logs the result, closing the loop between authentication, tool access, and audit that a gateway exists to enforce in the first place.
That sequence, verify identity through OAuth 2.1 with PKCE, resolve access through the enterprise IdP under EMA, then route the call through a catalog entry with credentials injected at runtime rather than stored in the clear, is what separates a gateway from a reverse proxy. A reverse proxy forwards traffic. A gateway configured this way decides whether that traffic should have been sent at all, and keeps a record of the decision.