Docker_MCP_Gateway_MultiUser_Architecture - TerrenceMcGuinness-NOAA/global-workflow GitHub Wiki
Docker MCP Gateway Multi-User Architecture Analysis
Document Version: 1.0
Date: January 12, 2026
Author: EIB MCP Team
Status: Analysis Complete - Awaiting Implementation Decision
Executive Summary
This document analyzes the Docker MCP Gateway architecture options for multi-user RDHPCS deployments where Subject Matter Expert (SME) developers access the MCP/RAG system via VS Code Remote Tunnels. After comprehensive investigation of the Docker MCP Gateway v0.35.0 source code, documentation, and design patterns, we present four architecture options with a recommended hybrid approach.
Key Finding: Container spawning per MCP session is the intended design of Docker MCP Gateway, not a bug. For multi-user VS Code scenarios, direct stdio connections provide better resource efficiency than gateway-mediated container spawning.
Background
Current Infrastructure
| Component | Status | Details |
|---|---|---|
| MCP Server | v7.1.1 | 35 tools, Node.js UnifiedMCPServer |
| ChromaDB | Healthy | 12 collections, 14,968 documents |
| Neo4j | Healthy | 2,744 files, 86,189 relationships |
| Docker MCP Gateway | v0.35.0 | Streamable HTTP transport, port 18888 |
| Target Users | 5-10 | SME developers via VS Code tunnels |
Observed Issue
During production use, we observed container accumulation:
docker ps --filter "label=docker-mcp=true"
NAMES STATUS
practical_golick Up 12 minutes (healthy) # Gateway-spawned
relaxed_mendeleev Up 38 minutes (healthy) # Gateway-spawned
stoic_grothendieck Up 2 days (healthy) # Gateway-spawned
eib-mcp-rag-static Up 3 days (healthy) # Systemd-managed
Each gateway-spawned container consumes 2GB memory, leading to resource exhaustion with multiple concurrent users.
Root Cause Analysis
Docker MCP Gateway Design Philosophy
From mcp-gateway/pkg/gateway/clientpool.go:
// Default behavior: spawn container per session
client = mcpclient.NewStdioCmdClient(cg.serverConfig.Name, "docker", env, runArgs...)
// With --long-lived: containers persist for gateway lifetime
return serverConfig.Spec.LongLived || cp.LongLived
// With --static: connect to pre-existing container via TCP
client = mcpclient.NewStdioCmdClient(cg.serverConfig.Name, "socat", nil,
"STDIO", fmt.Sprintf("TCP:mcp-%s:4444", cg.serverConfig.Name))
Mode Comparison
| Mode | Container Lifecycle | Connection Method | Cleanup |
|---|---|---|---|
| Default | Per tool call | docker run |
Immediate |
| --long-lived | Per gateway session | docker run |
Gateway stop |
| --static | Pre-started | socat TCP:4444 |
Manual |
| type: remote | External | HTTP/SSE | N/A |
Why --static Didn't Work
Our attempt to use --static mode failed because:
- Static mode expects container named
mcp-{server-name}(e.g.,mcp-eib-mcp-rag) - Container must run
docker-mcp-bridgeentrypoint - Bridge listens on TCP port 4444
- Gateway connects via
socat STDIO TCP:mcp-{name}:4444
Our container uses native stdio transport, not the TCP bridge.
Architecture Options
Option A: type: remote - Gateway Proxies to HTTP Server
How it works:
# ~/.docker/mcp/catalogs/eib-local.yaml
registry:
eib-mcp-rag:
name: eib-mcp-rag
type: remote # No container spawning
remote:
transport_type: streaming
url: http://localhost:3000 # Pre-existing HTTP server
title: EIB MCP RAG Server
Implementation Requirements:
- Add HTTP/Streaming transport to
UnifiedMCPServer.js - Run MCP server as systemd service on port 3000
- Gateway proxies all requests to single server
Pros:
- ✅ Single persistent server process
- ✅ No container spawning or lifecycle issues
- ✅ Remote clients automatically long-lived (v0.35.0 feature)
- ✅ Aligns with Docker's design for remote servers
Cons:
- ❌ Requires code changes to add HTTP transport
- ❌ Single point of failure (no container isolation)
Estimated Effort: 4-6 hours to add HTTP transport
Option B: Accept Default Behavior + Cleanup
How it works:
# Keep current configuration
docker mcp gateway run --servers eib-mcp-rag --transport streaming \
--port 18888 --long-lived
# Add cleanup cron (every hour)
0 * * * * docker ps -q --filter "label=docker-mcp=true" \
--filter "status=exited" | xargs -r docker rm
Pros:
- ✅ Works immediately without changes
- ✅ Standard Docker MCP Gateway behavior
- ✅ Session isolation
Cons:
- ❌ 2GB memory per container × concurrent sessions
- ❌ Cleanup is reactive, not proactive
- ❌ Poor scalability for 5-10 users
Estimated Effort: 30 minutes (add cron job)
Option C: Direct stdio (Skip Gateway for VS Code)
How it works:
// .vscode/mcp.json
{
"eib-mcp-rag-full": {
"type": "stdio",
"command": "node",
"args": ["mcp_server_node/src/UnifiedMCPServer.js", "full"],
"env": {
"CHROMADB_HOST": "localhost",
"CHROMADB_PORT": "8080",
"NEO4J_URI": "bolt://localhost:7687"
}
}
}
Pros:
- ✅ No containers - direct Node.js process (~200MB vs 2GB)
- ✅ Already working (current
eib-mcp-rag-fullconfiguration) - ✅ Lightweight per-session processes
- ✅ Shared databases (ChromaDB/Neo4j external)
Cons:
- ❌ No HTTP access for n8n/external clients
- ❌ No gateway security features
- ❌ Each session spawns Node.js process
Estimated Effort: None (already implemented)
Option D: Hybrid Architecture (Recommended)
How it works:
┌─────────────────────────────────────────────────────────────────────────┐
│ Multi-User MCP Architecture │
│ │
│ VS Code Sessions (SME Developers via Tunnels) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Dev A │ │ Dev B │ │ Dev C │ │
│ │ (Tunnel) │ │ (Tunnel) │ │ (Tunnel) │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ └─────────────┼─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────┐ │
│ │ Direct stdio (mcp.json) │ ~200MB per session │
│ │ node UnifiedMCPServer.js │ Lightweight, fast │
│ └─────────────┬───────────────┘ │
│ │ │
│ ════════════════════════════════════════════════════════════════════ │
│ │ │
│ External Clients (n8n, Claude Desktop, API) │
│ ┌──────────┐ ┌──────────┐ │
│ │ n8n │ │ Claude │ │
│ │ (5678) │ │ Desktop │ │
│ └────┬─────┘ └────┬─────┘ │
│ │ │ │
│ └──────┬──────┘ │
│ ▼ │
│ ┌───────────────────────────────┐ │
│ │ Docker MCP Gateway (:18888) │ │
│ │ type: remote │ │
│ └───────────────┬───────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────┐ │
│ │ HTTP MCP Server (:3000) │ Single persistent server │
│ │ UnifiedMCPServer.js │ systemd managed │
│ └───────────────┬───────────────┘ │
│ │ │
│ ┌─────────┴─────────┐ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ ChromaDB │ │ Neo4j │ │
│ │ :8080 │ │ :7687 │ │
│ └──────────┘ └──────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ supported_repos/global-workflow │ │
│ │ (Git submodule - GFS/GEFS operational code) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
Pros:
- ✅ VS Code gets fast, lightweight connections (stdio)
- ✅ External clients get gateway features (HTTP)
- ✅ Single HTTP server handles all gateway requests
- ✅ No container spawning for either path
- ✅ Databases shared across all access methods
Cons:
- ❌ Requires adding HTTP transport to server
- ❌ Two code paths (stdio + HTTP)
Estimated Effort: 4-6 hours
Recommendation
Phased Implementation Plan
Phase 1: Immediate (Today)
Use direct stdio for VS Code sessions
-
Disable gateway service:
sudo systemctl stop mcp-gateway sudo systemctl disable mcp-gateway -
Clean up accumulated containers:
docker ps -q --filter "label=docker-mcp=true" | xargs -r docker stop docker ps -aq --filter "label=docker-mcp=true" | xargs -r docker rm -
Verify VS Code mcp.json uses
eib-mcp-rag-full(stdio):{ "eib-mcp-rag-full": { "type": "stdio", "command": "node", "args": ["mcp_server_node/src/UnifiedMCPServer.js", "full"] } }
Outcome: Each developer's VS Code spawns lightweight Node.js process (~200MB)
Phase 2: Near-term (This Week)
Add HTTP/Streaming transport to UnifiedMCPServer.js
- Implement MCP HTTP transport handler
- Add
--transport http --port 3000CLI option - Create systemd service for HTTP MCP server
- Update gateway catalog to
type: remote
Phase 3: Complete (Next Week)
Full hybrid architecture operational
- VS Code → stdio (direct)
- External → gateway → HTTP server
- Single codebase, two transport modes
Technical References
Docker MCP Gateway Documentation
| Document | Purpose |
|---|---|
| mcp-gateway.md | Gateway usage guide |
| server-entry-spec.md | Catalog entry specification |
| examples/compose-static | Static mode example |
| profiles.md | v0.35.0 profiles feature |
Key Source Files
| File | Relevance |
|---|---|
pkg/gateway/clientpool.go |
Container lifecycle management |
pkg/gateway/run.go |
Static mode implementation |
pkg/catalog/types.go |
IsRemote() helper for remote servers |
Version Information
- docker-mcp plugin: v0.35.0 (built from source January 12, 2026)
- Key commit:
26d13965- "Remote Clients should be long-lived" - Repository: https://github.com/docker/mcp-gateway
Appendix: Server Entry Types
Type: server (Default)
name: my-mcp
type: server
image: my-org/my-mcp:latest
# Gateway spawns container per session
Type: remote (Recommended for Multi-User)
name: my-mcp
type: remote
remote:
transport_type: streaming
url: http://localhost:3000
# Gateway proxies to existing HTTP server
Type: poci (Per-Invocation)
name: my-tool
type: poci
tools:
- name: my-tool
container:
image: alpine:latest
# Gateway spawns container per tool call
Change Log
| Date | Version | Change |
|---|---|---|
| 2026-01-12 | 1.0 | Initial analysis and recommendations |