Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Appendix J: Docker and Infrastructure Templates

These templates package the notes server from Appendix C with Chapter 7’s conventions: Streamable HTTP on port 3000, /healthz for liveness and /readyz for readiness. They’re a starting point, not a production plan. Add image scanning, secret injection and your platform’s rollout policy.

Container Entry Point

A container must listen on 0.0.0.0, which turns off the SDK’s loopback-only Host/Origin defaults, so name the allowed values explicitly. Save as container.py beside server.py:

import os

from mcp.server.transport_security import TransportSecuritySettings

from server import mcp

# 0.0.0.0 turns off the SDK's loopback-only Host/Origin defaults, so name them.
security = TransportSecuritySettings(
    allowed_hosts=os.environ["MCP_ALLOWED_HOSTS"].split(","),
    allowed_origins=os.environ.get("MCP_ALLOWED_ORIGINS", "").split(","),
)

if __name__ == "__main__":
    mcp.run(transport="streamable-http", host="0.0.0.0", port=3000,
            json_response=True, stateless_http=True, transport_security=security)

Dockerfile

python:3.12-slim doesn’t include curl, so the healthcheck uses the Python that is already in the image.

FROM python:3.12-slim

COPY --from=ghcr.io/astral-sh/uv:0.8 /uv /usr/local/bin/uv
ENV UV_COMPILE_BYTECODE=1 PYTHONUNBUFFERED=1

WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-install-project
COPY server.py container.py ./

RUN useradd --create-home app && mkdir /data && chown app /data
USER app
ENV NOTES_DB_PATH=/data/notes.db

EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD ["python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:3000/healthz', timeout=3)"]
CMD ["/app/.venv/bin/python", "container.py"]

Local Compose File

services:
  notes:
    build: .
    ports:
      - "127.0.0.1:3000:3000"
    environment:
      MCP_ALLOWED_HOSTS: localhost:3000,notes.example.com
      MCP_ALLOWED_ORIGINS: https://notes.example.com
    volumes:
      - notes-data:/data

volumes:
  notes-data:

Publishing on 127.0.0.1 keeps the port off the LAN, which matters for a development server with no auth.

Kubernetes Skeleton

apiVersion: apps/v1
kind: Deployment
metadata:
  name: notes
spec:
  replicas: 2
  selector:
    matchLabels: {app: notes}
  template:
    metadata:
      labels: {app: notes}
    spec:
      containers:
        - name: notes
          image: notes:<version-tag>
          ports: [{containerPort: 3000}]
          env:
            - {name: MCP_ALLOWED_HOSTS, value: "notes.example.com"}
          readinessProbe:
            httpGet: {path: /readyz, port: 3000}
          livenessProbe:
            httpGet: {path: /healthz, port: 3000}
          resources:
            requests: {cpu: "100m", memory: "256Mi"}
            limits: {cpu: "500m", memory: "512Mi"}

MCP-Specific Infrastructure

  • No session affinity. 2026-07-28 has no protocol sessions, so any replica can answer any request. Turn sticky sessions off. If you still serve pre-2026-07-28 clients, their Mcp-Session-Id does need affinity or shared session storage.
  • Proxy buffering off for streamed responses: proxy_buffering off; in nginx, or nginx.ingress.kubernetes.io/proxy-buffering: "off" on ingress-nginx. A buffering proxy delivers every progress event at the end.
  • Ingress timeouts longer than your slowest tool call (proxy-read-timeout on ingress-nginx). A broken stream loses the request, and the client re-sends it.
  • Origin allowlist at the proxy and in MCP_ALLOWED_ORIGINS. Pass MCP-Protocol-Version, Mcp-Method and Mcp-Name through unchanged, or requests fail with -32020.