Skip to content

dev-proxy leaks stdio-mode Docker backends: 80 orphaned containers after two weeks #498

Description

@debugmcpdev

Summary

tools/dev-proxy/dev-proxy.mjs reaps orphaned Docker backends by published port only:

// _killDockerContainer()
if (!BACKEND_CMD || !BACKEND_CMD.match(/^docker\s+run/)) return;
const ids = execSync(`docker ps -q --filter publish=${BACKEND_PORT}`, ...)

A stdio-mode Docker backend (docker run -i --rm … stdio) publishes no port, so that filter never matches it and the container is never killed. _ensurePortFree() additionally returns early for stdio (if (this.backendTransport === 'stdio') return;), and _forceKillPid() kills the local docker client process — which detaches from, but does not stop, the container. --rm therefore never fires, because the containerised server process never exits.

Evidence

On a dev box that has been running the three Docker dev-proxy variants for two weeks:

$ docker ps -q | wc -l
84
# split by whether a port is published:
unpublished (stdio-mode): 80
published   (http/sse)  :  4      <- the 4 actually-live backends
$ docker inspect --format '{{.Name}} AutoRemove={{.HostConfig.AutoRemove}} Cmd={{json .Config.Cmd}} Started={{.State.StartedAt}}' <oldest>
/focused_mendel AutoRemove=true Cmd=["stdio"] Started=2026-08-12T21:09:38Z

Every orphan is Cmd=["stdio"]. The oldest had been running 14 days. Each holds a Node process plus loaded adapters, so the footprint grows with every proxy restart — and dev_rebuild_and_restart is called many times per session.

Repro

  1. Configure a dev-proxy with DEV_PROXY_BACKEND_TRANSPORT=stdio and a DEV_PROXY_BACKEND_CMD of docker run -i --rm mcp-debugger:local stdio.
  2. docker ps — one container.
  3. Call dev_restart_debugger.
  4. docker pstwo containers. The old one is still running.

Repeat for as many restarts as you like; they all stay.

Why the port filter can't be fixed in place

There is no port to filter on. The reap needs a different handle. Options, roughly in order of robustness:

  • Spawn the container with an explicit --name mcp-debugger-devproxy-<transport>-<pid> (or a --label devproxy=<id>) and reap with docker rm -f / --filter label=…. This makes the container addressable regardless of transport.
  • On shutdown, close the docker client's stdin and wait briefly before force-killing, so --rm can fire normally; keep the force-kill as the fallback.
  • Run _killDockerContainer() unconditionally on the stdio path too, using whichever handle the above provides, rather than early-returning in _ensurePortFree().

Impact

Dev-only (the proxy is not shipped to users), but it degrades the machine it runs on and silently: nothing in the proxy logs or dev_server_status reveals the accumulation. Found during a full /testdebugger everything sweep — noticed only because docker ps was run for an unrelated reason.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions