Skip to content

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

Closed as not planned

Description

@osmanpontes

Preflight Checklist

  • I have searched existing issues and this hasn't been reported yet
  • This is a single bug report (please file separate reports for different bugs)
  • 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:
    {
      "mcpServers": {
        "postgres": {
          "type": "stdio",
          "command": "docker",
          "args": ["run", "-i", "--rm", "crystaldba/postgres-mcp"]
        }
      }
    }
  2. Start a Claude Code session (the MCP container starts automatically)
  3. Close/exit the Claude Code session
  4. Run docker ps — the container is still running
  5. 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.

Activity

  1. github-actions commented on Feb 26, 2026

    @github-actions

    Found 3 possible duplicate issues:

    1. [BUG] MCP servers not properly terminated when Claude Code exits, causing orphaned processes #1935
    2. MCP containers (github-mcp-server) not cleaned up after session ends #16397
    3. MCP server processes are never cleaned up when sessions end — orphan accumulation causes OOM #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

  2. osmanpontes commented on Feb 26, 2026

    @osmanpontes
    Author
  3. chkp-petert commented on Mar 12, 2026

    @chkp-petert

    I have exactly same issue on macOS

  4. tabletenniser commented on Mar 28, 2026

    @tabletenniser

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

  5. yurukusa commented on Mar 29, 2026

    @yurukusa

    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.

  6. QinyunWang commented on Apr 8, 2026

    @QinyunWang

    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.

  7. lambdamusic commented on May 15, 2026

    @lambdamusic

    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..

  8. localden commented on May 23, 2026

    @localden
    Collaborator

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

  9. github-actions commented on Jul 12, 2026

    @github-actions

    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.

  10. locked as resolved and limited conversation to collaborators on Jul 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:mcpbugSomething isn't workinghas reproHas detailed reproduction stepsplatform:linuxIssue specifically occurs on Linux

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions