MCP Server Docker Deployment in 3 Steps — SSE Deprecated, Now Streamable HTTP
Prerequisites for this article: It is assumed that the Python MCP server is operating normally locally in STDIO mode. Basic knowledge of Docker and Python will make it easier to follow along.
Problems begin the moment the entire team shares an MCP server that was running smoothly on a local machine. Complaints like "It works on my computer" are exchanged, environment variables are repeatedly set, and when one person upgrades a library, another's server crashes. This article covers the entire process of solving these issues using Docker containerization. What sets this article apart is that it addresses the background and countermeasures regarding the deprecation of SSE Transport and the establishment of Streamable HTTP as the new standard as of 2025. By distinguishing between solutions for legacy environments and the latest recommended methods, it provides practical guidelines that can be immediately applied to real-world scenarios, ranging from Dockerfile writing to criteria for selecting Transport.
Key Concepts
MCP (Model Context Protocol)란?
MCP is a standard protocol released as open source by Anthropic, an interface defined to enable LLM applications to connect with external data sources and tools in a consistent manner. It standardizes communication protocols between clients (AI assistants such as Claude and Cursor) and servers (tool providers such as file systems, DBs, and APIs).
┌──────────────┐ JSON-RPC 2.0 ┌────────────────────┐
│ MCP Client │◄─────────────────►│ MCP Server │
│ (AI Assistant)│ Transport Layer │ (Tools / Resources)│
└──────────────┘ └────────────────────┘MCP’s Core Value: By standardizing tool integration methods, you can build a reusable tool ecosystem that is not dependent on specific AI products.
Evolution of Transport Technology: STDIO → SSE → Streamable HTTP
| Transport | Communication Method | Purpose | Status |
|---|---|---|---|
| STDIO | Bidirectional (stdin/stdout) | Local Single Client | ✅ Local Recommended |
| HTTP + SSE | Client→Server: HTTP POST, Server→Client: SSE Stream | Remote Multi-Client | ⚠️ Deprecated |
| Streamable HTTP | Single HTTP endpoint — Selectably serve responses as SSE stream or plain JSON | Remote multiple clients | ✅ Current standard |
SSE Transport Structure: A two-channel structure where the client connects to /sse to open a server push stream, and the command is sent via a separate HTTP POST.
Client ──── GET /sse ─────────────────────► Server
◄─── text/event-stream ─────────────
──── POST /messages (JSON-RPC) ────►Why was SSE deprecated? SSE had structural overhead requiring the maintenance of two separate channels (SSE reception + POST transmission), and handling reconnection in case of disconnection was complex. Streamable HTTP integrates this into a single /mcp endpoint.
Streamable HTTP Structure: When a client sends an HTTP POST request, the server responds as an SSE stream or plain JSON depending on the Accept header. It is a flexible structure based on unidirectional HTTP request-response that can stream the response.
Client ──── POST /mcp (Accept: text/event-stream) ───► Server
◄─── SSE 스트림 (또는 일반 JSON 응답) ──────────Session Management and Multiple Clients
When multiple clients connect simultaneously in a team environment, Streamable HTTP ensures isolation between clients by assigning a session ID to each request. Since the Python SDK's StreamableHTTPSessionManager manages session-specific state independently, client A's requests are not mixed with client B's responses. Unlike STDIO, sessions are tracked over stateless HTTP, so the context can be restored using the session ID even if the connection is disconnected and reconnected.
Legacy Client Support: supergateway
If you still need to use a client that supports only SSE Transport, utilize supergateway. It is a protocol conversion gateway that exposes an STDIO-based MCP server as an SSE server without code modification.
npx supergateway --stdio "python -m my_mcp_server" --port 8000supergateway: STDIO ↔ SSE protocol conversion gateway. Useful when exposing an existing STDIO MCP server via HTTP during the migration transition.
Practical Application
Writing a Multi-stage Dockerfile
A simple Dockerfile includes both build tools and runtime files, making the image bloated. Using a multi-stage build allows you to leave only the files necessary for execution in the final image.
# ── 1단계: 빌드 스테이지 ──────────────────────────────────────
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
# --user 플래그로 /root/.local에 설치 (다음 스테이지로 복사 용이)
RUN pip install --user --no-cache-dir -r requirements.txt
# ── 2단계: 런타임 스테이지 ────────────────────────────────────
FROM python:3.12-slim
# 보안: root가 아닌 전용 사용자 생성
RUN useradd -m -u 1000 mcp-user
WORKDIR /app
# 빌드 스테이지에서 설치된 패키지만 복사
COPY --from=builder /root/.local /home/mcp-user/.local
# 소유권 지정과 함께 애플리케이션 코드 복사
COPY --chown=mcp-user:mcp-user . .
USER mcp-user
ENV PATH=/home/mcp-user/.local/bin:$PATH
EXPOSE 8000
# 헬스체크: python:3.12-slim에는 curl이 없으므로 Python 내장 urllib 활용
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
CMD ["python", "-m", "mcp_server"]| Components | Roles |
|---|---|
AS builder |
Separation of build-only stage — pip, gcc, etc. are not included in the final image |
useradd -m -u 1000 |
Dedicated user without root privileges — Minimize damage upon container exit |
COPY --from=builder |
Selectively copy build artifacts only — reduce image size |
HEALTHCHECK (urllib) |
python:3.12-slim does not include curl — safely implemented with Python built-in module |
Confidential Information Warning: Make sure to write .dockerignore so that the COPY . . command does not include the .env file in the image layer.
# .dockerignore
.env
.env.*
__pycache__/
*.pyc
.git/
.venv/
*.egg-info/Build Verification: After docker build -t my-mcp:latest . succeeds, check the image size with docker inspect my-mcp:latest --format='{{.Size}}'. When applying a multi-stage build, it is normal for the image size to be reduced to less than half compared to a single stage.
Configuring a Team Shared Environment with Docker Compose
To use the same MCP server for the entire team, define the service as docker-compose.yml and separate sensitive information into the .env file.
# docker-compose.yml
# Docker Compose v2에서는 version 필드가 불필요합니다 (공식 문서 권장 제거)
services:
mcp-server:
build:
context: .
dockerfile: Dockerfile
ports:
- "8000:8000"
environment:
- MCP_HOST=0.0.0.0
- MCP_PORT=8000
- MCP_TRANSPORT=streamable-http
- API_KEY=${API_KEY} # .env 파일에서 주입
- DATABASE_URL=${DATABASE_URL}
restart: unless-stopped
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
interval: 30s
timeout: 10s
retries: 3
deploy:
resources:
limits:
memory: 512M # 메모리 상한 — OOM 보호
cpus: '0.5'
# 필요 시 nginx 리버스 프록시 추가 (TLS 종단)
nginx:
image: nginx:alpine
ports:
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/ssl/certs:ro
depends_on:
- mcp-serverTLS Required: When exposed over HTTP, API keys and query content are transmitted in plain text. You must use Nginx or Caddy as a reverse proxy to terminate the TLS.
# .env.example (팀 공유 — 실제 값은 팀원 각자 .env에 설정)
API_KEY=your_api_key_here
DATABASE_URL=postgresql://user:pass@host:5432/db
# 팀원이 서버를 시작하는 명령
cp .env.example .env # API 키 등 환경변수 설정
docker compose up -d # 백그라운드 실행
docker compose logs -f # 로그 실시간 확인Switching STDIO/Streamable HTTP to FastMCP
This is a pattern where you quickly test with STDIO during local development and switch to Streamable HTTP for production deployment. The Python MCP SDK recommends the ASGI-based approach of mcp.server.fastmcp. Internally, StreamableHTTPSessionManager handles session isolation and streaming responses.
# mcp_server.py
import os
from mcp.server.fastmcp import FastMCP
# FastMCP로 서버 정의 (Transport에 무관하게 동일)
mcp = FastMCP("my-mcp-server")
@mcp.tool()
async def search_docs(query: str) -> str:
"""내부 문서를 검색합니다"""
return await do_search(query)
async def do_search(query: str) -> str:
"""검색 로직 구현 예시 — 실제 환경에 맞게 교체하세요"""
# 예: DB 쿼리, 벡터 검색, Elasticsearch 호출 등
return f"'{query}'에 대한 검색 결과: [관련 문서 목록]"
# ── Transport 선택: 환경변수로 분기 ──────────────────────────
if __name__ == "__main__":
transport = os.getenv("MCP_TRANSPORT", "stdio")
if transport == "streamable-http":
# 프로덕션: Streamable HTTP
# FastMCP는 내부적으로 StreamableHTTPSessionManager와
# Starlette ASGI 앱을 구성한 뒤 uvicorn으로 실행합니다.
mcp.run(
transport="streamable-http",
host=os.getenv("MCP_HOST", "0.0.0.0"),
port=int(os.getenv("MCP_PORT", "8000")),
)
else:
# 로컬 개발: STDIO
mcp.run(transport="stdio")/health Endpoint: Essential for Docker and Kubernetes health checks. Add the /health route to the ASGI app exposed by FastMCP, or use the mcp.custom_route() decorator. If deployed without this endpoint, traffic will be routed to the server being initialized.
# 로컬 개발 (STDIO)
python mcp_server.py
# Docker 컨테이너 (Streamable HTTP)
MCP_TRANSPORT=streamable-http python mcp_server.py| Components | Roles |
|---|---|
FastMCP |
High-level MCP Server Builder — Declaratively Register Tools with Decorators |
@mcp.tool() |
Tool Registration — Automatically generate inputSchema from function signature |
mcp.run(transport=...) |
Select Transport Execution — Internally configure ASGI app and launch uvicorn |
do_search() |
Separation of actual business logic — Replacement with actual implementations such as DB, search engine, etc. |
Pros and Cons Analysis
Advantages
| Item | Content |
|---|---|
| Environmental Consistency | Development, test, and production operate identically on the python:3.12-slim image |
| Team Collaboration | docker compose up All teams can use the same server with a single line |
| Dependency Isolation | Guaranteed independent runtimes without system Python or pip version conflicts |
| Version Management | Clearly track deployment versions with image tags (v1.2.3), making rollback easy |
| Horizontal Scaling | Auto-scaling based on traffic with Kubernetes HPA or Docker Swarm |
| Cloud Portability | Deploy identically on any platform including Azure Container Apps, Fly.io, Render, etc. |
Disadvantages and Precautions
| Item | Content | Response Plan |
|---|---|---|
| SSE Transport Legacy | Deprecated as of 2025, unsuitable for new projects | Streamable HTTP first, SSE maintained only for legacy clients |
| Secret Management | Hardcoding API keys in the Dockerfile permanently exposes them to image layers | .dockerignore + .env + Use of Docker Secrets is mandatory |
| Alpine Compatibility | C extension packages (numpy, cryptography, etc.) may conflict with Alpine musl libc | Python server python:3.12-slim (Debian-based) is recommended |
| Operational Complexity | Requires Graceful shutdown, log collection, and process supervision | Uses tini as PID 1, applies structured logging |
| TLS Absence | Transmit API key/query content in plaintext when HTTP is exposed | TLS endpoint processing via nginx/Caddy reverse proxy |
Distroless Image: For security-critical environments, consider using gcr.io/distroless/python3 instead of python:3.12-slim. The shell and package manager are removed, minimizing the attack surface.
tini: Running a Python process directly with PID 1 in a Docker container causes issues with SIGTERM handling and zombie process collection. Resolve this by adding the docker run --init flag or tini to the Dockerfile as the init process.
The Most Common Mistakes in Practice
- Do not add
.envto.dockerignore—COPY . .includes.envcontaining the API key in the image layer. Since you can check the layer contents withdocker history <image>, be sure to check.dockerignorebefore building. - Applying SSE Transport to New Projects — If you choose SSE without a clear reason to support legacy clients, you will soon be burdened with the debt of migrating deprecated APIs.
/healthEndpoint Not Implemented — Without a health check endpoint, Docker and Kubernetes cannot determine the container's readiness and route traffic to the server that is being initialized.
In Conclusion
If, before reading this article, you viewed the MCP server as a "tool that runs only on my computer," you should now be able to view it as a production service that the entire team can trust and share. If container boundaries have eliminated environmental inconsistencies and the criteria for selecting a transport (Streamable HTTP vs. SSE) have become clear, the purpose of this article has been achieved.
3 Steps to Start Right Now:
- Write a Multi-stage Dockerfile — Write a Dockerfile using the
FROM python:3.12-slim AS builderpattern and add.dockerignore. After building, check the image size withdocker inspect my-mcp:latest --format='{{.Size}}'and verify that it is less than half the size of a single stage. - Write docker-compose.yml and .env.example — Write in Compose v2 format without the
versionfield, then check if the team shared environment starts successfully withdocker compose up -d. If thecurl http://localhost:8000/healthresponse is{"status": "ok"}, it is a success. - Switch Transport to Streamable HTTP — Branch STDIO/HTTP using the
MCP_TRANSPORTenvironment variable, and connect tohttp://localhost:8000/mcpfrom Claude or the MCP client in use to verify end-to-end whether the tool call works correctly.
Next Post: How to run an MCP server with HPA (Horizontal Pod Autoscaling) on Kubernetes and monitor call latency and error rates with Prometheus + Grafana
Reference Materials
- Build to Prod MCP Servers with Docker | Docker Blog
- MCP Server Best Practices | Docker Blog
- MCP Practical Guide with SSE Transport | F22 Labs
- How to Build and Deploy a Model Context Protocol MCP Server | Northflank
- How to Run MCP Servers with Docker | Snyk
- Understanding MCP Recent Change Around HTTP+SSE | Christian Posta
- Exposing MCP Tools Remotely Using SSE | Medium
- Running MCP Servers on Containers using Finch | AWS Dev Community
- supergateway — STDIO↔SSE Gateway | GitHub
- Model Context Protocol Official Documentation