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-Iddoes need affinity or shared session storage. - Proxy buffering off for streamed responses:
proxy_buffering off;in nginx, ornginx.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-timeouton ingress-nginx). A broken stream loses the request, and the client re-sends it. Originallowlist at the proxy and inMCP_ALLOWED_ORIGINS. PassMCP-Protocol-Version,Mcp-MethodandMcp-Namethrough unchanged, or requests fail with-32020.