This guide explains how to use Docker with the Debug MCP Server, allowing you to run the server in a container alongside other MCP servers like the GitHub MCP Server.
We provide a Dockerfile and build scripts for creating a Docker container with all necessary dependencies pre-installed, including Node.js and Python with debugpy.
docker build -t mcp-debugger:local .Or using the npm script:
npm run docker-buildWhen running the Debug MCP Server in a Docker container, you should mount your project files to /workspace inside the container. This is the default and recommended mount point. The container sets MCP_WORKSPACE_ROOT=/workspace and MCP_CONTAINER=true by default. MCP_CONTAINER=true enables container-mode behaviors such as path rewriting and pre-loading of known adapters, while MCP_WORKSPACE_ROOT is used for path resolution. The mount is optional -- the image creates an empty /workspace so volume-less runs (e.g. kubectl debug ephemeral containers) still have a valid workspace directory -- but the variable is not: in container mode getWorkspaceRoot() (src/utils/container-path-utils.ts) throws when MCP_WORKSPACE_ROOT is unset, so path resolution has no fallback to hide the mistake. (A few adapters do keep their own MCP_WORKSPACE_ROOT || '/workspace' fallback for narrower jobs — source-map derivation in the rust and cpp adapters, the default cwd in the javascript adapter -- but that never rescues the path-resolution throw.) The image supplies it twice over (a Dockerfile ENV, plus scripts/docker-entry.sh re-applying ${MCP_WORKSPACE_ROOT:-/workspace} at startup), so this only bites if you override the entrypoint or point the variable at a path that isn't mounted.
The Debug MCP Server resolves paths through centralized container path utilities (src/utils/container-path-utils.ts). When running in a container (MCP_CONTAINER=true), the server performs centralized container path rewriting: resolvePathForRuntime() rewrites paths to be under the workspace root (MCP_WORKSPACE_ROOT, which the image sets to /workspace; required in container mode -- see above). Non-workspace absolute paths (e.g., /home/user/test.py) are rewritten to fall under the workspace root rather than being rejected. SimpleFileChecker then validates existence and returns the resolved effectivePath, which the server passes downstream to the debug adapter. This means:
- Your project files must be mounted at
/workspace - The LLM should provide paths relative to the project root or as Linux-style absolute paths under
/workspace - The server rewrites paths to the container workspace root; it does not perform Windows-to-Linux path conversion (e.g.,
C:\Users\...paths will not be correctly translated inside the container) - Debug adapter handles its own path resolution natively after receiving the rewritten path
Once the image is built, you can run the server with volume mounts:
docker run -i --rm -v /path/to/your/project:/workspace:rw mcp-debugger:local stdioHere's the recommended configuration for your MCP settings file:
{
"mcpServers": {
"mcp-debugger-docker": {
"command": "docker",
"args": [
"run",
"--rm",
"-i",
"-v",
"/path/to/your/project:/workspace:rw",
"mcp-debugger:local",
"stdio"
],
"autoApprove": [
"create_debug_session",
"list_supported_languages",
"list_debug_sessions",
"set_breakpoint",
"list_breakpoints",
"remove_breakpoint",
"clear_breakpoints",
"start_debugging",
"restart_debugging",
"attach_to_process",
"detach_from_process",
"expose_session",
"unexpose_session",
"close_debug_session",
"step_over",
"step_into",
"step_out",
"continue_execution",
"pause_execution",
"list_threads",
"get_variables",
"get_local_variables",
"get_stack_trace",
"get_scopes",
"evaluate_expression",
"get_source_context",
"get_output",
"redefine_classes"
],
"disabled": false,
"timeout": 60
}
}
}- Replace
/path/to/your/projectwith the actual path to the project you want to debug - The
:rwsuffix allows read-write access (required for debugging) - The Docker entrypoint (
scripts/docker-entry.sh) runsdist/bundle.cjsand passes through command-line arguments (e.g.,stdio). It does not hardcode--log-levelor--log-file - When using the debugger, provide paths relative to the project root (e.g.,
examples/python/fibonacci.pynot/workspace/examples/python/fibonacci.py) - Optional env flags pass through with
-e, e.g.-e DEBUG_MCP_BP_ADDRESSING=linerestricts breakpoint addressing features (default: all enabled; see the set_breakpoint section of the tool reference),-e DEBUG_MCP_NO_REDACT=1to disable the default masking of credential-shaped values in variable/evaluate/output results, or-e DEBUG_MCP_VARIABLE_ACCESS=explicitto require explicit variable names on get_variables/get_local_variables (see the Secret redaction and Least-privilege mode sections of the tool reference)
The image vendors CodeLLDB for the image's own architecture — linux-x64 or linux-arm64, selected from TARGETARCH at build time; the published image is built for both (one shared copy under @debugmcp/codelldb-common, resolved via CODELLDB_PATH) — and ships g++, so Rust and C/C++ debugging work inside the container (issue #328):
- C/C++ source-file launch: pass a lone
.c/.cppas the program and the adapter compiles it in-container with-gdwarf-4 -O0into.debug-mcp/next to the source. - Prebuilt binaries: must be Linux-compiled for the image's architecture (x86-64 for the amd64 image, aarch64 for the arm64 one). Binaries compiled on a Windows/macOS host and mounted into the container are not debuggable by container LLDB — that's a binary-format fact, not a packaging gap. Cross-compile for Linux or compile in-container.
- Attach by PID: works for native processes inside the container. Attaching to a non-descendant process needs
--cap-add=SYS_PTRACEwhen the host kernel setskernel.yama.ptrace_scope >= 1(Kuberneteskubectl debug --profile=generalgrants the equivalent — see the Kubernetes debugging recipe). - Rust remains launch-only (the rust adapter has no attach implementation).
- Rust type summaries work out of the box (issue #441): the image vendors the Rust toolchain's LLDB formatter scripts at
/opt/rust-sysroot/lib/rustlib/etcand setsCODELLDB_RUST_SYSROOT=/opt/rust-sysroot, which the rust adapter translates into CodeLLDB'slang.rust.sysrootsetting — so&str/String/Vecvalues render as values without anyrustcin the image. Override with-e CODELLDB_RUST_SYSROOT=/path/to/sysroot(a sysroot root whoselib/rustlib/etcholds the formatters), or disable with-e CODELLDB_RUST_SYSROOT=to fall back to CodeLLDB's normalrustc --print sysrootlookup. Caveat: the vendored formatters track the Rust version pinned in the Dockerfile; a debuggee built by a much newer/older rustc may render some std types imperfectly.
The image ships the Ruby adapter without a Ruby runtime. Launching Ruby scripts in the container is therefore unavailable, but attach works: Ruby attach is a direct TCP connection to a running rdbg --open DAP socket, so no local Ruby is needed. list_supported_languages reports this per-mode (ruby.modes.launch.available: false, ruby.modes.attach.available: true).
To attach from the container, the rdbg target's socket must be reachable from inside it — e.g. run both on one docker network and attach by container name, or add --add-host=host.docker.internal:host-gateway and attach to a port on the host. See docs/ruby/README.md (Remote attach) for the full flow, and the Kubernetes debugging recipe for the in-pod sidecar variant of the same idea.
To use both servers together, configure them in your MCP settings:
{
"mcpServers": {
"mcp-debugger-docker": {
"command": "docker",
"args": [
"run",
"--rm",
"-i",
"-v",
"/path/to/your/project:/workspace:rw",
"mcp-debugger:local",
"stdio"
],
"disabled": false,
"autoApprove": [],
"timeout": 60
},
"github": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server"
],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "your-github-token"
},
"disabled": false,
"autoApprove": [],
"timeout": 60
}
}
}If you need to debug files from multiple locations, you can mount multiple directories under /workspace:
"args": [
"run",
"--rm",
"-i",
"-v",
"/path/to/project1:/workspace/project1:rw",
"-v",
"/path/to/project2:/workspace/project2:rw",
"mcp-debugger:local",
"stdio"
]Then reference files as project1/file.py or project2/script.js.
To expose the debugpy port for remote debugging:
docker run -i --rm -p 5679:5679 -v /path/to/project:/workspace:rw mcp-debugger:local stdioIn the MCP settings:
"args": [
"run",
"-i",
"--rm",
"-p",
"5679:5679",
"-v",
"/path/to/project:/workspace:rw",
"mcp-debugger:local",
"stdio"
]Two environment variables control when a containerized server gives up and exits. Both are
passed through with -e:
MCP_EXIT_ON_STDIN_CLOSE=1(src/cli/stdin-watchdog.ts) -- opt-in orphan self-defense for the network transports (http,sse), which otherwise have no reason to care about stdin. When set to1ortrue, the server watches its stdin forend/close/errorand shuts down gracefully when it sees one, with a 5s backstop that force-exits if shutdown stalls. Intended for a supervisor that spawns the server with a stdin pipe (tools/dev-proxydoes exactly this) and needs a parent-death signal that also works on Windows. Strictly opt-in: without the variable, a detached server whose stdin is closed keeps running. In a container it is only safe with a stdin pipe attached. The network-transport watchdog has no container or client-traffic exemption (unlike thestdiocommand's), sodocker run -d -e MCP_EXIT_ON_STDIN_CLOSE=1 ... http-- where stdin is/dev/nulland reports EOF immediately -- shuts the server down at startup. Pass-i(or omit the variable) for detached network-mode containers. It is unrelated to thestdio-mode exit policy described under Troubleshooting, which is always on and needs no variable.MCP_HTTP_STALE_SESSION_MS(src/cli/http-command.ts) -- idle window before an HTTP MCP session with no open SSE stream is reaped, closing its debug sessions and releasing any ptrace claim on the target. Defaults to 30 minutes (1800000);0disables that path; invalid values log a warning and fall back to the default. Read only byhttpmode (the legacyssecommand has no reaper). A short-lived diagnostic container wants a much tighter window -- see the Kubernetes debugging recipe, which sets it to 5 minutes.MCP_HTTP_STREAM_LOST_SESSION_MS-- the short window for a session whose SSE stream dropped and never came back (default 2 minutes;0falls back to the stale window). An MCP SDK client keeps a GET stream open, so when it crashes or exits withoutDELETE /mcpthe server sees the socket close and releases its debug sessions -- including a paused attach target -- within this window plus one sweep, instead of holding them for the full stale window (issue #658). Only a client that never opened a stream waits outMCP_HTTP_STALE_SESSION_MS.GET /healthlists what each HTTP session is holding.
The Dockerfile is multi-stage. Every base image is pinned by digest, so read the
FROM lines in the Dockerfile for the exact versions rather than
trusting a version quoted here:
builder(anode:*-slimbase) installs pnpm via corepack, builds the workspace packages, vendors the CodeLLDB engine for the target architecture, and bundles the server into/app/dist/bundle.cjsrust-formatters(arust:*-slimbase) exists only to copy the Rust toolchain's LLDB formatter scripts out of$(rustc --print sysroot)/lib/rustlib/etc(issue #441)- The runtime stage (an
ubuntu:*base) installs the debug toolchains the image ships -- Python 3 plus a hash-pinned debugpy, LLDB andpython3-lldb,g++, and a headless JDK 21 -- then copies in the Node binary, the bundle, the runtime adapter packages, and the vendored CodeLLDB from the earlier stages - It sets the environment the server reads at runtime:
MCP_CONTAINER=true,MCP_WORKSPACE_ROOT=/workspace,CODELLDB_PATH,CODELLDB_RUST_SYSROOT, andDEBUG_MCP_DISABLE_LANGUAGES=go,dotnet(no Delve, no netcoredbg in the image) tiniis PID 1, in front of thescripts/docker-entry.shwrapper; the default command isstdio- The application runs from
/app, keeping/workspacefree for user mounts
This ensures all dependencies needed for both Node.js execution and Python debugging are available in the container.
-
"File not found" errors:
- Ensure your files are mounted to
/workspace, not other paths like/app/project - Check that the mount syntax is correct:
-v /host/path:/workspace:rw - Verify the host path exists and has proper permissions
- Ensure your files are mounted to
-
Path resolution problems:
- The server expects paths relative to
/workspace - If you provide
test.py, the server looks for/workspace/test.py - Non-workspace absolute paths like
/home/user/test.pyare rewritten under the workspace root (e.g.,/workspace/home/user/test.py), which is unlikely to exist -- use relative paths or paths already under/workspaceinstead
- The server expects paths relative to
-
Permission issues:
- On Unix-based systems, you might need to adjust file permissions
- Consider using
:rwsuffix for read-write access - Check that the Docker daemon has access to the host directories
-
Container not terminating (and
--rmnever firing):- In stdio mode, stdin is the MCP transport, so an EOF on it means the client is
gone. Since issue #633 the server acts on that: on stdin
end/close/errorit stops all debug sessions (bounded to 5s) and exits 0, which is what letsdocker run --rmremove the container.SIGTERM/SIGINTtake the same path, sodocker stopis still a clean shutdown. - There is exactly one exemption, in
src/cli/stdio-command.ts: ifMCP_CONTAINER=trueand not a single byte has ever arrived on stdin, EOF is ignored and the server stays alive. That coversdocker runwithout-i, where stdin is already closed before any client can speak. Once any client traffic has been seen, that case is ruled out and EOF always means exit. - Host mode has no exemption -- EOF on stdin always shuts the server down.
- So always pass
-i. Without it the container has no way to notice the client leaving, runs forever, and every session leaks a container. If you find containers piling up, check thedocker runarguments for a missing-ifirst, then usedocker ps/docker stop <container_id>to clean up the strays.
- In stdio mode, stdin is the MCP transport, so an EOF on it means the client is
gone. Since issue #633 the server acts on that: on stdin
-
Port already in use:
- If port 5679 is already in use, you can map to a different port:
docker run -i --rm -p 5680:5679 mcp-debugger:local -
Build failures:
- Ensure Docker daemon is running
- Check available disk space
- Try clearing Docker cache:
docker system prune
For more general troubleshooting, see troubleshooting.md.