Safeguard
AI Security

Docker + MCP: Running MCP Servers in Containers Securely

MCP servers run with your credentials and your filesystem unless you say otherwise. Containerizing them with read-only mounts, dropped capabilities, and egress controls turns an open-ended trust grant into a bounded one.

Yukti Singhal
Head of Product
6 min read

Running MCP servers inside containers — the pattern most people shorthand as Docker MCP — is the single most effective hardening step for teams adopting the Model Context Protocol, because a containerized server gets exactly the filesystem, network, and privileges you grant instead of inheriting your entire user session. An MCP server launched bare with npx runs as you: your SSH keys, your cloud credentials, your home directory. The same server in a locked-down container sees only what its job requires. This guide covers how to build that boundary properly.

Why run MCP servers in Docker at all?

MCP servers are, structurally, arbitrary code that an AI agent invokes on your behalf. Most are installed straight from npm or PyPI with a one-line command, which means you are executing unaudited third-party code with the full permissions of your local user or CI runner. The risk is not hypothetical: the supply-chain attack pattern that hit npm repeatedly — typosquats, hijacked maintainer accounts, install-script payloads — applies unchanged to MCP server packages.

Containers address the blast radius, not the trust decision. A Docker MCP deployment gives you four controls a bare process never has: a pinned, content-addressed image instead of "whatever version npx resolved today"; a filesystem boundary; a network boundary; and resource limits. Docker's own MCP Catalog and Toolkit, launched in 2025, formalized this pattern by distributing MCP servers as container images — a signal that the ecosystem is converging on containers as the default packaging for MCP.

What does a hardened Docker MCP container look like?

The baseline flags cost nothing and remove most of the attack surface:

docker run -i --rm \
  --read-only \
  --cap-drop=ALL \
  --security-opt no-new-privileges \
  --user 10001:10001 \
  --memory 512m --pids-limit 256 \
  --mount type=bind,src=/work/project,dst=/data,ro \
  ghcr.io/example/mcp-server@sha256:3f9a...

Each line closes a specific hole. --read-only stops the server from persisting payloads or modifying its own code. --cap-drop=ALL removes Linux capabilities the server will never need. no-new-privileges blocks setuid escalation. A non-root --user means a compromise lands in an unprivileged account. Memory and PID limits contain runaway or malicious resource use. Pinning by digest — not by tag — guarantees the image you reviewed is the image you run.

Two anti-patterns to refuse outright: never mount the Docker socket (/var/run/docker.sock) into an MCP container, which hands it root-equivalent control of the host, and never bind-mount your home directory when a single project folder will do. If the server genuinely needs write access, mount one specific writable volume and keep everything else read-only.

How should secrets reach a containerized MCP server?

Most useful MCP servers need credentials — a GitHub token, a database URL, an API key. The failure mode is baking these into the image or passing long-lived, broad-scope tokens through environment variables that every process in the container can read.

Better practice, in ascending order of effort: pass short-lived tokens at docker run time via --env-file from a file outside version control; scope every token to the minimum permission the server's tools require (a read-only GitHub token for a code-search server, not a repo-admin PAT); and for orchestrated deployments, use your platform's secret store so credentials never touch disk in plaintext. Rotate on a schedule, because an MCP server that logs verbosely may echo credentials into logs you ship elsewhere.

How do you control network egress from MCP containers?

Egress is the exfiltration path. A compromised or prompt-injected MCP server that can reach any host can ship your data anywhere. Docker gives you workable options: --network none for servers that do pure local computation, custom bridge networks that only route to the specific internal services a server needs, or an explicit egress proxy that allowlists destinations for servers that must reach one external API.

The discipline matters most for servers that combine two properties: access to private data and the ability to make outbound requests. That combination — private data in, attacker-readable channel out — is precisely the setup that indirect prompt injection exploits. Cutting egress to a named allowlist converts "can leak anywhere" into "can only talk to the API it exists to call."

What risks remain even with containers?

Containers bound what a server can touch; they do nothing about whether the server should be trusted with what it can touch. Three risks survive containerization. Tool poisoning: a malicious server can lie in its tool descriptions to manipulate the model into misusing other tools. Prompt injection through tool output: content a server fetches — a web page, an issue comment — can carry instructions the model may follow, and those instructions execute with whatever access your other connected tools have. And plain data misuse: a server you grant read access to a repository can summarize that code to its own backend regardless of how well it is sandboxed.

So the container is necessary, not sufficient. Review what you connect, prefer catalog-vetted images over unknown packages, keep scanning the images themselves for vulnerable dependencies the way you scan any other artifact — our take on wiring that into CI is covered across the Safeguard blog — and treat agent-written code and agent-fetched content as untrusted input. We run our own MCP server in production at Safeguard, and it gets the same SAST and DAST scrutiny as every other service.

FAQ

What is the Docker MCP Catalog?

It is Docker's curated registry of MCP servers packaged as container images, paired with the MCP Toolkit for running them from Docker Desktop. The practical benefit is provenance: you pull a versioned, inspectable image from a known namespace instead of executing whatever an npx command resolves at run time.

Should an MCP server ever mount the Docker socket?

Almost never. Socket access is equivalent to root on the host — anything that can talk to the Docker daemon can start a privileged container and escape. If an MCP server legitimately needs to manage containers, isolate it on a dedicated VM or use a scoped proxy in front of the API rather than the raw socket.

Is stdio or HTTP better for containerized MCP servers?

Stdio (docker run -i) is simpler and keeps the server unreachable from the network, which is ideal for local, single-client use. HTTP transports suit shared or remote deployments but add an authentication and TLS burden you must actually carry — an unauthenticated HTTP MCP server on a routable interface is an open tool-execution endpoint.

Do containers stop prompt injection against MCP tools?

No. Injection is an attack on the model's instruction-following, not on the process boundary. Containers limit what a hijacked toolchain can reach — that is exactly why egress control and minimal mounts matter — but defenses against injection itself live in tool design, output handling, and human confirmation for consequential actions.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.