Run your MCP servers in a box

An MCP server is a program that runs as you, with your keys and your network. How to put each one in a container, and what that does and doesn't protect.

Containers for AI, part 2: Run your MCP servers in a box. A faint diagram of one box linked to three small boxes, with a glowing dot in the middle one.

MCP servers are how coding agents get tools: GitHub, a database, a browser, your files. Adding one is a line in a config file, usually something like npx some-mcp-server. That line starts a program on your machine, as you.

What that program can reach

An MCP server started with npx or uvx runs with your user’s permissions. It can read your SSH keys and cloud credentials, see every environment variable the agent was started with, read any file you can, and connect anywhere on the internet. It needs none of that to list your GitHub issues.

What an MCP server can reach. Run with npx, it runs as you and can reach your SSH keys, AWS credentials, every environment variable, every file you own and the whole internet. The same server in a container gets one token, one read-only folder and one allowed host, with no home folder and no other secrets.

This has already gone wrong. In September 2025, a popular npm package called postmark-mcp, which let agents send email, shipped an update that quietly blind-copied every message to an outside address. Researchers at Koi Security spotted it, and the package was pulled from npm. A flaw in mcp-remote, a widely used helper, let a malicious server run commands on the developer’s machine (CVE-2025-6514, rated 9.6). Dozens more MCP vulnerabilities were reported in the first half of 2026.

Boxing one server by hand

Most MCP clients only need a command that speaks MCP over standard input and output. docker run -i is such a command. Here is GitHub’s official server in Claude Code’s .mcp.json:

{
  "mcpServers": {
    "github": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "--read-only", "--cap-drop=ALL",
        "--security-opt", "no-new-privileges",
        "--memory", "512m",
        "-e", "GITHUB_PERSONAL_ACCESS_TOKEN",
        "ghcr.io/github/github-mcp-server"
      ],
      "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}" }
    }
  }
}

What each part buys you:

  • -i --rm: talk over stdin and stdout, and throw the container away when the session ends.
  • No -v mounts: the server can’t see your home folder. If it needs files, mount one folder, read-only: -v "$PWD/docs:/docs:ro".
  • -e with one name: the container gets that one token, not your whole environment.
  • --read-only --cap-drop=ALL --security-opt no-new-privileges: it can’t modify its own image or gain privileges.
  • --network none for servers that work only on local files, such as a filesystem or SQLite server.

Pin the image by digest (ghcr.io/github/github-mcp-server@sha256:…), so an update reaches you only when you choose it. That alone would have stopped a postmark-mcp-style update.

Many servers: a gateway

Once you have five servers, five blocks of docker run flags get unwieldy. A gateway is one MCP server that fronts all the others. Your client connects to it once. It starts each real server in its own container when a tool is first called, hands each one only its own secret, and logs every call.

One gateway, many small boxes. An AI client such as Claude Code, Codex or Cursor connects to an MCP gateway that starts servers, adds each secret and logs every call. Below it, three containers: github with GITHUB_TOKEN and access to api.github.com; postgres with DB_URL and access to db:5432 only; browser with no secrets and web access but no LAN.

Docker’s MCP Gateway works this way: secrets are set with docker mcp secret set and injected only into the container that needs them. The log matters as much as the isolation. When an agent does something odd, you can see which tool it called, with what.

What the box doesn’t fix

A container limits what a server can reach. It doesn’t limit what the server is allowed to do with the access you gave it. A GitHub token that can delete repositories still can, from inside a container. So:

  • Give each server the smallest token that works: a fine-grained, read-only token for a server that only reads issues.
  • Treat tool output as untrusted text. A server that fetches web pages or issues can hand your agent instructions written by a stranger. That is a prompt-injection problem, and no container solves it on its own.