Lesson 13 Docker & HTTP Transport
Theme:

Homework — Lesson 13

Three tasks · healthcheck, Streamable HTTP probe, image-size experiment · ~60–90 min

Lesson 13 turned PKB-TS into a Docker-deployable HTTP+SSE server sitting behind Caddy. The lab walked you through the PKB_TRANSPORT branch in server.ts, the multi-stage Dockerfile, the .dockerignore that keeps the build context small, and the six-line Caddyfile that terminates TLS in front of the Node process. These three homework tasks push the same surface in three operational directions: (1) make the compose stack self-aware with a Docker healthcheck so Caddy waits for PKB to be ready; (2) read the SDK source for the newer Streamable HTTP transport and write a short comparison — no code change, just a reading-the-source exercise; (3) run three Dockerfile variants and record image sizes so you have real numbers when you defend the multi-stage / Alpine choice in a code review.

Before you start

Lesson 13's lab must be done end-to-end: docker compose up from mcp-servers/sample-server/typescript/ brings up pkb + caddy, the Inspector connects over the Caddy hostname (HTTPS on the staging issuer), and curl -sk https://<your-hostname>/healthz returns {"ok":true}. You need a working Docker daemon (Docker Desktop on macOS, Docker Engine on Linux) and roughly 2 GB of free disk for Task 3's three image variants. No new npm dependencies are required.

Task 1 — Add a Docker healthcheck

Extend docker-compose.yml so the pkb service has a healthcheck that hits /healthz every 10 s with a 2 s timeout and 3 retries, and make caddy wait for pkb to be healthy before starting. Without this, Caddy can come up first, fail its upstream dial, and either crash-loop or cache a stale 502 — you have seen both in the wild.

Requirements:

Bring the stack up clean with docker compose down && docker compose up and watch the order: pkb should reach (healthy) first, then Caddy starts.

✓ Self-check: docker compose up logs show pkb-pkb-1 reach (healthy) before Caddy's first log line; docker inspect pkb-pkb-1 --format='{{.State.Health.Status}}' prints healthy; if you kill the Node process inside the container (docker exec pkb-pkb-1 kill 1 — PID 1 is Node), the status flips to unhealthy within ~30 s and Compose restarts it (because of restart: unless-stopped).

Task 2 — Streamable HTTP probe (reading exercise)

The MCP spec has been moving away from the two-endpoint SSE design toward a single-endpoint Streamable HTTP transport. PKB-TS uses SSE today because it is the transport L13 introduces; the newer one is what Cloudflare Workers, Vercel Functions, and Deno Deploy actually prefer. Do not migrate the code. This is a reading-the-source exercise — the deliverable is a short summary.

Open the SDK file directly from your node_modules:

code node_modules/@modelcontextprotocol/sdk/server/streamableHttp.js
# or, if you only have less:
less node_modules/@modelcontextprotocol/sdk/server/streamableHttp.js

Read the StreamableHTTPServerTransport class top to bottom (it is short; the .d.ts next to it is the cleanest reference for the public surface). Then write a 12–20-line summary in your own words that answers all three of:

  1. Wire shape. How does Streamable HTTP differ from SSE on the wire? SSE uses a long-lived GET /sse for server-to-client plus a separate POST /messages for client-to-server. Streamable HTTP collapses those into one endpoint — describe how.
  2. Why edge runtimes prefer it. Cloudflare Workers, Vercel Functions, and Deno Deploy all have short per-request CPU/connection budgets and limited concurrent open streams per worker. Name one concrete reason a long-lived SSE GET is hostile to that model, and one reason chunked HTTP is friendlier.
  3. Migration step. Name one concrete change you would have to make in PKB-TS's server.ts to swap SSEServerTransport for StreamableHTTPServerTransport. (One bullet is enough — you are not implementing this.)

The summary must cite at least one specific SDK identifier (a class name, function name, or HTTP route) by name — this is how the self-check distinguishes "actually read it" from "summarised a blog post".

✓ Self-check: your summary is 12–20 lines; it names at least one identifier from the SDK source verbatim (e.g., StreamableHTTPServerTransport, handleRequest, or the exact HTTP method + path used); it does not describe the transport as "WebSockets" — it is plain HTTP chunked transfer, and saying otherwise means you read a summary, not the source.

Task 3 — Image-size reduction experiment

The Lesson 13 Dockerfile is multi-stage with a node:20-alpine runtime. That is the recommended configuration, but "recommended" is more convincing when you have measured the alternatives. Build three variants, record the resulting image sizes, and write the comparison as a markdown table.

Variants:

  1. Baseline. As-is — multi-stage with node:20-alpine runtime. Tag it pkb-ts:alpine-multi.
  2. Single-stage on full node:20. One stage, no FROM ... AS build / FROM ... AS runtime split — build and run live in the same image. Tag it pkb-ts:node-single. Expect roughly 3× the baseline.
  3. Multi-stage with node:20-bookworm-slim runtime. Same multi-stage shape as the baseline, but swap the runtime base from Alpine to Debian Slim. Tag it pkb-ts:slim-multi. Expect roughly +30% over the Alpine baseline.

To compare cold-build times fairly, prune Docker's build cache between variants:

docker builder prune -af
docker build -t pkb-ts:alpine-multi -f Dockerfile .
docker builder prune -af
docker build -t pkb-ts:node-single -f Dockerfile.single .
docker builder prune -af
docker build -t pkb-ts:slim-multi -f Dockerfile.slim .
docker images | grep pkb-ts

Record the output as a 3-row markdown table with columns Variant, Final image size, and Build time (cold). Use the docker images "SIZE" column for the first number and a wall-clock time docker build ... for the second.

✓ Self-check: all three images build successfully and run (docker run --rm -e PKB_TRANSPORT=stdio pkb-ts:<tag> answers an Inspector connect); the alpine multi-stage variant is the smallest; the single-stage variant on full node:20 is the largest; if the slim variant comes out smaller than Alpine, double-check you actually swapped the runtime base and not the build base.

View solutions →