[BUG] Docker-based MCP server containers not stopped when session ends

Status Closed — not planned
Reported on v2.1.59
Maintainer reply ✓ Yes — localden
Activity 9 comments · opened Feb 26, 2026 · closed May 23, 2026
💡 Likely answer: A maintainer (localden, collaborator) responded on this thread — see the highlighted reply below.

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

When an MCP server is configured to run via docker run in .mcp.json, the Docker container keeps running after closing Claude Code. This does not happen with non-Docker MCP servers (e.g., npx-based), which are properly cleaned up as direct child processes.

The root cause: docker run is the child process of Claude Code, but the actual container runs under the Docker daemon. Killing the docker run process does not kill the container.

A SessionEnd hook workaround is unsafe because stopping containers by image name (e.g., docker stop $(docker ps -q --filter ancestor=image-name)) would kill containers used by other concurrent Claude Code sessions — there's no way to map a container to a specific session.

What Should Happen?

The Docker container should be stopped when the Claude Code session ends, just like npx-based MCP servers are terminated.

Suggested fix: Claude Code could capture the Docker container ID when starting a Docker-based MCP server and run docker stop <container_id> during session cleanup. This would safely stop only the container belonging to the ending session.

Steps to Reproduce

  1. Configure a Docker-based MCP server in .mcp.json:

``json
{
"mcpServers": {
"postgres": {
"type": "stdio",
"command": "docker",
"args": ["run", "-i", "--rm", "crystaldba/postgres-mcp"]
}
}
}
``

  1. Start a Claude Code session (the MCP container starts automatically)
  2. Close/exit the Claude Code session
  3. Run docker ps — the container is still running
  4. Compare: configure an MCP server with npx instead of docker — it gets cleaned up properly on session end

Claude Model

Opus

Is this a regression?

I don't know

Claude Code Version

2.1.59

Platform

Anthropic API

Operating System

Other Linux

Terminal/Shell

Other

Additional Information

  • OS: Arch Linux (6.18.9-arch1-2)
  • Shell: fish
  • The --rm flag on docker run only removes the container after it stops, but nothing triggers the stop on session end.

View original on GitHub ↗

9 Comments

github-actions[bot] · 6 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/1935
  2. https://github.com/anthropics/claude-code/issues/16397
  3. https://github.com/anthropics/claude-code/issues/26658

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

osmanpontes · 6 months ago
chkp-petert · 5 months ago

I have exactly same issue on macOS

tabletenniser · 5 months ago

+1, same issue on MacOS, can we prioritize this?

yurukusa · 5 months ago

A Stop hook can clean up Docker containers when the session ends:

docker ps --filter "name=mcp" --format '{{.ID}}' 2>/dev/null | while read id; do
    docker stop "$id" 2>/dev/null
    echo "Stopped MCP container: $id" >&2
done
docker ps --filter "label=started-by=claude-code" --format '{{.ID}}' 2>/dev/null | while read id; do
    docker stop "$id" 2>/dev/null
done
exit 0
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
if echo "$COMMAND" | grep -qE 'docker\s+run'; then
    CONTAINER=$(docker ps -l --format '{{.ID}}' 2>/dev/null)
    if [ -n "$CONTAINER" ]; then
        echo "$CONTAINER" >> "$HOME/.claude/.docker-containers"
    fi
fi
exit 0
{
  "hooks": {
    "Stop": [{"hooks": [{"type": "command", "command": "bash ~/.claude/hooks/docker-cleanup.sh"}]}],
    "PostToolUse": [{"matcher": "Bash", "hooks": [{"type": "command", "command": "bash ~/.claude/hooks/docker-tag.sh"}]}]
  }
}

The Stop hook fires on session end (normal completion, /exit, Ctrl+C) and stops any MCP containers. The PostToolUse hook tracks which containers were started during the session for reliable cleanup.

QinyunWang · 4 months ago

Workaround: Wrapper script + per-session PID watchdog

Each Docker MCP points to a small wrapper script instead of docker directly. The wrapper:

  1. Captures CLAUDE_PID=$PPID — the Claude Code process that spawned it.
  2. Generates a unique container name scoped to that PID.
  3. Spawns a detached watchdog using Python's os.setsid() (works on both macOS and Linux) that polls kill -0 $CLAUDE_PID every 2 s. When Claude exits by any means, the watchdog calls docker stop on that container and terminates itself.
  4. execs docker run with the named container + two labels.

Step-by-step setup

1. Create the wrapper script

Create .claude/scripts/mcp-docker-launch.sh in your project (adjust path as needed):

#!/usr/bin/env bash
# Launch a Docker-based MCP server with a self-terminating watchdog.
# Usage in .mcp.json: set "command" to this script and pass docker image args
# (without the leading "run" -- this script adds it).
set -euo pipefail

CLAUDE_PID="$PPID"
CONTAINER_NAME="claude-mcp-${CLAUDE_PID}-$$-${RANDOM}"

# Detached watchdog: polls Claude Code's PID and stops the container when it dies.
# Uses Python's os.setsid() to escape Claude's process group — setsid command is
# Linux-only and unavailable on macOS. nohup + disown prevent SIGHUP on shell exit.
nohup python3 -c "
import os, sys, subprocess, time
os.setsid()
pid, name = int(sys.argv[1]), sys.argv[2]
while True:
    try:
        os.kill(pid, 0)
        time.sleep(2)
    except (ProcessLookupError, PermissionError):
        break
subprocess.run(['docker', 'stop', name], capture_output=True)
" "$CLAUDE_PID" "$CONTAINER_NAME" </dev/null >/dev/null 2>&1 &
disown || true

exec docker run -i --rm \
  --name "$CONTAINER_NAME" \
  --label claude-mcp=true \
  --label "claude-pid=${CLAUDE_PID}" \
  "$@"

Make it executable:

chmod +x .claude/scripts/mcp-docker-launch.sh

2. Update .mcp.json

For each Docker-based MCP, change "command": "docker" to the wrapper and remove the leading "run" from args (the wrapper adds it):

{
  "mcpServers": {
    "my-mcp": {
      "type": "stdio",
      "command": "/absolute/path/to/.claude/scripts/mcp-docker-launch.sh",
      "args": [
        "-e", "SOME_ENV_VAR",
        "my-docker-image",
        "--some-flag"
      ],
      "env": {
        "SOME_ENV_VAR": "value"
      }
    }
  }
}

Any Docker MCP that uses this wrapper gets automatic cleanup.

lambdamusic · 3 months ago

I'm on a mac, after installing the latest Docker v.4.7.30 somehow symlinks got updated and that caused mayhem.

What I had to do to fix Claude is the following:

In claude_desktop_config.json, update the "mcpServers" and "MCP_DOCKER" section

from

            "command": "docker",

to

            "command": "/Users/my-name/.docker/bin/docker",

I did have a global symlink to "/Users/my-name/.docker/bin/docker", but apparently Claude cannot pick that up from my .bash_profile.

So hardcoding the full path to the Docker executable did the trick..

localden collaborator · 3 months ago

Thanks for the report. This is the same issue as #1935 (stdio MCP child processes not terminated on exit). Consolidating tracking there.

github-actions[bot] · 1 month ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.