Skip to main content

How to Run a Local MCP Server in a Sandbox

Short answer

A local MCP server is a program on your computer, so it can read anything your user account can read, including your files and the environment variables you gave it. To limit that, run the server in a Docker container with no network, a read-only file system, all Linux capabilities dropped and one folder mounted for its data. Your AI app still starts the server and talks to it over stdin and stdout, the same way it would talk to a bare process. The docker run command in step 3 is the whole setup.

Tested withMCP spec 2026-07-28Python SDK (mcp) 2.3.0MCP Inspector 2.11.0Docker Engine 28.4.0Python 3.14 (python:3.14-slim base image)Node.js 24.19 (the Inspector needs 22.19 or later)Checked 20 Oct 2026

What you'll have at the end​

The bookmarks server from Build an MCP server, running in a container that has no network, can't write outside one folder and holds no Linux privileges. Your AI app starts it and calls its tools as before:

These are the checks from step 5, run against the container. Each line shows one limit working:

10001
CapEff: 0000000000000000
touch: cannot touch '/etc/x': Read-only file system
no eth0 interface

Before you start

  • Docker: Docker Desktop on macOS or Windows, or Docker Engine on Linux, running. We tested with Docker Engine 28.4.0.
  • The bookmarks server: a bookmarks.py file from Build an MCP server, Python version. You don't need its pyproject.toml or uv for this guide.
  • Node.js 22.19 or later, for the MCP Inspector check in step 4.
  • To connect it at the end: Claude Desktop, or any app from MCP clients.

1. Write the Dockerfile​

Save this as Dockerfile in the same folder as bookmarks.py:

Dockerfile
FROM python:3.14-slim

# Python can't write .pyc files into the read-only image, so turn them off.
# The bookmarks file lives in /data, the only place the server may write.
ENV PYTHONDONTWRITEBYTECODE=1 \
BOOKMARKS_FILE=/data/bookmarks.json

RUN pip install --no-cache-dir "mcp[cli]>=2.3.0" \
&& useradd --uid 10001 --no-create-home app \
&& mkdir /data \
&& chown app:app /data

WORKDIR /app
COPY bookmarks.py .

USER app
ENTRYPOINT ["python", "bookmarks.py"]

The server runs as a user called app with ID 10001, so it isn't root inside the container. The image's own files belong to root, so the server can read its code but can't change it. The only folder it can write to is /data. BOOKMARKS_FILE points the server there: the server already reads that variable, so the Python code from the reference guide doesn't change.

PYTHONDONTWRITEBYTECODE=1 stops Python from trying to write cache files into the read-only image.

2. Build the image​

docker build -t bookmarks-sandbox .

Run this in the folder that holds Dockerfile and bookmarks.py. The first build downloads the Python base image from Docker Hub.

3. Run it with the limits​

This is the command your AI app will run. Each flag removes a power the server doesn't need, or limits one it does:

docker run --rm -i \
--network none \
--read-only \
--tmpfs /tmp \
--cap-drop ALL \
--security-opt no-new-privileges \
--memory 256m \
--cpus 0.5 \
--pids-limit 64 \
-v bookmarks-data:/data \
bookmarks-sandbox
FlagWhat it does
--rmRemoves the container when the server exits.
-iKeeps stdin open. The AI app sends its messages on stdin, so stdio needs this.
--network noneCuts the container off from the network, so it can't make connections to other hosts. Use it for servers that only work with local files.
--read-onlyMakes the container's root file system read-only.
--tmpfs /tmpGives the container a scratch folder in memory. It's wiped when the container stops.
--cap-drop ALLRemoves every Linux capability. Capabilities are the fine-grained privileges root normally has. A stdio server needs none of them.
--security-opt no-new-privilegesStops processes in the container from gaining new privileges.
--memory 256m, --cpus 0.5, --pids-limit 64Cap memory, CPU time and the number of processes, so a runaway server can't take over your machine.
-v bookmarks-data:/dataMounts one named Docker volume at /data. It's the only place the server can save data, and it survives after the container stops.

Docker's run reference documents each option, and the resource constraints page explains the memory and CPU limits.

4. Test it with MCP Inspector​

MCP Inspector runs the same command your AI app will run. Put the Docker command after --cli, and the Inspector's own flags after the image name. List the tools with the current protocol revision:

npx -y @modelcontextprotocol/inspector@2.11.0 --cli docker run --rm -i --network none --read-only --tmpfs /tmp --cap-drop ALL --security-opt no-new-privileges --memory 256m --cpus 0.5 --pids-limit 64 -v bookmarks-data:/data bookmarks-sandbox --protocol-era modern --method tools/list

You should see save_bookmark and search_bookmarks in the output. Run it again with --protocol-era legacy to confirm older apps can connect too.

Then save a bookmark and search for it. Each call starts a new container, which reads the same bookmarks-data volume, so the second call finds the first call's bookmark:

npx -y @modelcontextprotocol/inspector@2.11.0 --cli docker run --rm -i --network none --read-only --tmpfs /tmp --cap-drop ALL --security-opt no-new-privileges --memory 256m --cpus 0.5 --pids-limit 64 -v bookmarks-data:/data bookmarks-sandbox --protocol-era modern --method tools/call --tool-name save_bookmark --tool-arg url=https://modelcontextprotocol.io --tool-arg title="MCP docs"
npx -y @modelcontextprotocol/inspector@2.11.0 --cli docker run --rm -i --network none --read-only --tmpfs /tmp --cap-drop ALL --security-opt no-new-privileges --memory 256m --cpus 0.5 --pids-limit 64 -v bookmarks-data:/data bookmarks-sandbox --protocol-era modern --method tools/call --tool-name search_bookmarks --tool-arg query=mcp

The search returns - MCP docs: https://modelcontextprotocol.io.

5. Check the limits​

Run a shell inside the same container to confirm each limit is in place. This command checks the user, the capabilities, the read-only root file system and the network:

docker run --rm --network none --read-only --tmpfs /tmp --cap-drop ALL --security-opt no-new-privileges --memory 256m --cpus 0.5 --pids-limit 64 -v bookmarks-data:/data --entrypoint sh bookmarks-sandbox -c 'id -u; grep CapEff /proc/self/status; touch /etc/x 2>&1; grep "eth0:" /proc/net/dev || echo "no eth0 interface"'

The four lines of output are the ones at the top of this guide. The first is the user ID, 10001. The second shows no capabilities. The third is the write attempt to /etc failing, and the fourth shows there's no eth0 interface, which is the network Docker gives a container by default. To see that the last check does work, run the same command without --network none. The eth0 line appears.

Your files aren't inside the container either. On macOS, /Users doesn't exist in it:

docker run --rm --entrypoint sh bookmarks-sandbox -c 'ls /Users'

The output is ls: cannot access '/Users': No such file or directory. The container sees only the image and the volume you mount.

6. Connect it to Claude Desktop​

Claude Desktop reads its servers from claude_desktop_config.json. Open the file for your system and create it if it doesn't exist:

OSConfig file
macOS~/Library/Application Support/Claude/claude_desktop_config.json
Windows%APPDATA%\Claude\claude_desktop_config.json
Linux~/.config/Claude/claude_desktop_config.json

Add the server under the mcpServers key. The args are the same flags as step 3:

claude_desktop_config.json
{
"mcpServers": {
"bookmarks": {
"command": "docker",
"args": [
"run",
"--rm",
"-i",
"--network",
"none",
"--read-only",
"--tmpfs",
"/tmp",
"--cap-drop",
"ALL",
"--security-opt",
"no-new-privileges",
"--memory",
"256m",
"--cpus",
"0.5",
"--pids-limit",
"64",
"-v",
"bookmarks-data:/data",
"bookmarks-sandbox"
]
}
}
}

Start Docker first, because Claude Desktop runs docker and the Docker daemon has to be up. Save the file, then quit Claude Desktop completely and reopen it. If Claude Desktop can't start the server, check its MCP logs as described in Build an MCP server, and if the logs say docker can't be found, put the full path from which docker in command.

Ask Claude to bookmark a page, then ask it what you've bookmarked. The data lives in the bookmarks-data volume, which Docker keeps when a container is removed, so it's still there after Claude restarts the server.

Tighten it further​

  • Mount read-only data as read-only. A server that only reads files should get its folder with :ro after the path, for example -v "$HOME/notes:/data:ro". In our test, writes to a :ro mount failed with Read-only file system. The bookmarks server writes, so it keeps the writable volume.
  • Pin the image by digest. Once you push the image to a registry, refer to it as image@sha256:... instead of a tag. The MCP guide says a digest freezes the whole dependency tree.
  • Give the server its own account. The MCP guide also lists a separate user account as a way to isolate a server. The container's non-root user already keeps it away from your files, but a dedicated OS account is a second layer. This guide doesn't cover setting one up.
  • Allow only the network the server needs. --network none stops an API call. A server that calls an API needs a network, and Docker's default network lets containers make outgoing connections. Remove --network none and send traffic through an egress proxy or firewall with a domain allowlist, as the MCP guide recommends.

Common problems​

Cannot connect to the Docker daemon at ... Is the docker daemon running? Docker isn't running. Start Docker Desktop, or the Docker service on Linux, and try again.

touch: cannot touch '/app/x': Permission denied The server runs as the non-root app user, which doesn't own /app, so it can't write there. Save data to /data, the folder the volume is mounted on. If a tool needs a scratch folder, add a --tmpfs for it.

touch: cannot touch '/data/x': Read-only file system The folder is mounted read-only. Remove :ro from the -v flag if the server needs to write there.

Claude Desktop doesn't show the server. Confirm that Docker is running and that the JSON is valid, then restart Claude Desktop fully. The reference guide's Common problems section covers the log files and the full-path fix for command.

FAQ​

Does running an MCP server in a container make it safe?

Not on its own. A container limits what the server can reach, so a bad or overreaching server sees less of your computer. It can't tell whether the server uses the access it has honestly, so you still need to read tool descriptions and approve sensitive tool calls.

Does Claude Desktop still work with a server in a container?

Yes. Claude Desktop starts the container with the same stdio transport it uses for any local server, as long as Docker is running when it does.

Can a server in a sandbox reach the internet?

Not with --network none. A server that calls an API needs a network, so drop that flag and limit its outbound traffic with an egress proxy or firewall that allows only the domains it needs.

Do I need a sandbox for a server I wrote myself?

It still helps. The MCP guidance treats a local server as code that can overreach, and prompt injection can steer an honest server into using more access than its job needs. A sandbox limits the damage either way.