Dockerize Your App
Buildable now. The repo, the spec and the deployment steps are live — the written walkthrough for this one is still being drafted.
Containerize the streaming chat API from earlier with a multi-stage Dockerfile, a persistent SQLite volume, and a health check. This is the project that makes everything you’ve built so far deployable instead of “runs on my machine” — the next two projects take that container to real cloud infrastructure.
Containerization is a baseline expectation for any engineering role, AI or not — this closes the most common gap between 'I built a project' and 'I can ship a project.'
Path: 1
Position: 10 of 12
Difficulty: 🟡 Requires Docker Desktop (no cloud account needed)
Estimated time: 2–3 hours
AWS cost: None
Agent Pickup Instructions
This spec is self-contained. Build this project without reading any other spec file.
mkdir -p path-1/p1-10-dockerize
cd path-1/p1-10-dockerize
# Build and run
docker build -t chat-api .
docker run -p 8000:8000 --env-file .env chat-api
# Or with compose (includes persistent SQLite volume)
docker compose up
# Verify
curl http://localhost:8000/health
curl -N -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"message": "hello"}'
docker compose down
Done when:
-
docker build -t chat-api .completes successfully (image < 500MB) -
docker compose upstarts the API and health check passes - SQLite data persists to a named volume (survives
docker compose restart) -
docker compose downstops cleanly - Health check in docker-compose.yml marks container unhealthy after 3 failures
- README includes exactly what to set in
.envbefore running - No file in
src/exceeds 200 lines
What this project is
A focused Docker project that containerises the streaming chat API from Project 8. The learner writes a Dockerfile, adds a docker-compose.yml with a persistent SQLite volume and health check, and verifies the entire application runs identically in a container as it does locally. This is the only new skill needed before AWS Lambda in Project 11 — Lambda's Lambda-compatible container requirement is no longer surprising once you've built a Docker image.
What the learner achieves
"I containerised a FastAPI application with a multi-stage Dockerfile, added a SQLite volume for persistence, and configured a health check — the image is under 500MB and starts in under 3 seconds."
Folder structure
p1-10-dockerize/
├── README.md
├── GUIDE.md
├── .env.example
├── .env ← gitignored; copied from .env.example
├── .dockerignore
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
└── src/
├── main.py ← FastAPI app (same as p1-08, fully included here)
├── llm.py
├── database.py
├── rate_limiter.py
└── config.py
.env.example
LLM_PROVIDER=anthropic
LLM_API_KEY=
LLM_MODEL=
RATE_LIMIT_REQUESTS_PER_MINUTE=20
COST_PER_1M_INPUT_TOKENS=1.00
COST_PER_1M_OUTPUT_TOKENS=5.00
DATABASE_PATH=/data/chat_requests.db
Note: DATABASE_PATH points to /data/ — this directory is a Docker named volume.
requirements.txt
anthropic==0.40.0
openai==1.58.0
ollama==0.4.4
fastapi==0.115.6
uvicorn==0.34.0
sse-starlette==2.2.1
tiktoken==0.8.0
python-dotenv==1.0.1
Dockerfile
# Stage 1: build dependencies
FROM python:3.12-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --target /build/deps -r requirements.txt
# Stage 2: runtime image
FROM python:3.12-slim AS runtime
WORKDIR /app
# Copy dependencies from builder (keeps image smaller than pip install at runtime)
COPY --from=builder /build/deps /usr/local/lib/python3.12/site-packages/
# Copy application code
COPY src/ src/
# Create data directory (will be overridden by volume mount)
RUN mkdir -p /data
# Non-root user for security
RUN useradd -m appuser && chown -R appuser:appuser /app /data
USER appuser
EXPOSE 8000
# Health check: call /health endpoint every 30s
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
CMD ["python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]
docker-compose.yml
version: "3.9"
services:
chat-api:
build: .
ports:
- "8000:8000"
env_file:
- .env
volumes:
- chat-data:/data
restart: unless-stopped
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
volumes:
chat-data:
driver: local
.dockerignore
.env
.git
__pycache__
*.pyc
*.pyo
.pytest_cache
tests/
*.db
.index/
sessions/
src/ — what to implement
The src/ directory contains the full streaming chat API from p1-08. Include ALL files:
src/main.py— FastAPI app with/health,/chat,/statssrc/llm.py— streaming provider routersrc/database.py— SQLite init and loggingsrc/rate_limiter.py— sliding window rate limitersrc/config.py— reads all env vars
All implementation details match p1-08-streaming-chat-api.md exactly. This project adds the Dockerfile and docker-compose.yml on top of that working application. The one change: DATABASE_PATH defaults to /data/chat_requests.db (matches the Docker volume mount).
tests/ — what to test
There are no new pytest tests in this project — the application logic is already tested in p1-08. The verification is operational:
Operational test 1 — image builds:
docker build -t chat-api . must complete without error. Check image size with docker images chat-api.
Operational test 2 — health check passes:
After docker compose up, run docker compose ps. The health column must show healthy within 60 seconds.
Operational test 3 — volume persists across restart:
Make a chat request. docker compose restart. curl http://localhost:8000/stats. Confirm total_requests is non-zero (data survived restart).
Operational test 4 — environment variable injection:
Set RATE_LIMIT_REQUESTS_PER_MINUTE=1 in .env. docker compose up. Make 2 rapid requests. Second request should return 429.
Include a scripts/verify.sh that runs all four checks and reports pass/fail.
scripts/verify.sh (include in project)
#!/bin/bash
set -e
echo "=== Docker Verification Script ==="
echo "[1/4] Building image..."
docker build -t chat-api . --quiet
echo "✓ Image built"
echo "[2/4] Starting with compose..."
docker compose up -d
sleep 15 # wait for startup
echo "[3/4] Checking health..."
STATUS=$(docker compose ps --format json | python3 -c "import sys,json; d=json.load(sys.stdin); print(d[0]['Health'])" 2>/dev/null || echo "unknown")
if [ "$STATUS" = "healthy" ]; then
echo "✓ Container healthy"
else
echo "⚠ Health status: $STATUS (may still be starting)"
fi
echo "[4/4] Testing endpoints..."
HEALTH=$(curl -s http://localhost:8000/health)
echo "Health: $HEALTH"
echo ""
echo "=== Making test request ==="
curl -N -s -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"message": "Say the word hello"}' \
-H "Accept: text/event-stream" &
CURL_PID=$!
sleep 5
kill $CURL_PID 2>/dev/null || true
echo ""
echo "=== Checking stats ==="
curl -s http://localhost:8000/stats | python3 -m json.tool
echo ""
docker compose down
echo "✓ Compose stopped"
echo "=== All checks done ==="
README.md content
# Dockerize Your App
Packages the streaming chat API into a Docker container with a persistent SQLite
volume and health check. Runs identically on any machine with Docker installed.
## Prerequisites
- Docker Desktop (https://www.docker.com/products/docker-desktop/)
- An LLM API key (or Ollama running locally)
## Setup
```bash
cd p1-10-dockerize
cp .env.example .env
# Edit .env: set LLM_API_KEY (and LLM_PROVIDER if not anthropic)
Run
# Start everything
docker compose up
# In another terminal — test it
curl http://localhost:8000/health
curl -N -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"message": "Say hello"}'
# Stop
docker compose down
Expected:
chat-api | INFO: Application startup complete.
chat-api | INFO: Uvicorn running on http://0.0.0.0:8000
Verify everything works
bash scripts/verify.sh
What to try next
- Check image size:
docker images chat-api— should be under 500MB - Change RATE_LIMIT_REQUESTS_PER_MINUTE=1 in .env, restart, try 2 rapid requests
- This image is what you'll deploy to Lambda in the next project
---
## GUIDE.md content
```markdown
# Build guide: Dockerize Your App
## What you're building and why it matters
Docker is the unit of deployment for most cloud platforms. AWS Lambda, ECS, Cloud Run,
and Kubernetes all accept container images. If you can build a container that runs
your application, you can deploy it anywhere. The two concepts that trip people up:
multi-stage builds (keeping the final image small by not including build tools) and
volumes (keeping data outside the container so it survives restarts). Both are in
this project.
## The decision that matters in this build
**Multi-stage Dockerfile.** A single-stage build installs pip, gcc, and all build
tools in the final image. A multi-stage build installs them in a temporary builder
stage and copies only the compiled packages to the final image. The result: the image
that runs in production doesn't include anything that isn't needed at runtime.
This matters for security (smaller attack surface) and for pull time on cold starts.
## What will break
**`DATABASE_PATH` must point inside the named volume.** If it points to `/app/`
(the application directory), data is stored inside the container layer and lost on
restart. The Dockerfile creates `/data/` and the compose file mounts the named volume
there. Check that `.env` has `DATABASE_PATH=/data/chat_requests.db` — the old default
was `./chat_requests.db`, which would silently use the container layer.
**HEALTHCHECK timing.** Docker marks a container unhealthy after `retries` consecutive
failures. With `start_period=10s`, Docker ignores failures in the first 10 seconds.
If your app takes 15 seconds to start (dependency downloads, model loads), the container
will be marked unhealthy even if it is fine. Adjust `start_period` to match your startup time.
## How to talk about this in an interview
"I containerised a FastAPI application with a multi-stage Dockerfile — the final image
is under 500MB because build tools don't ship to production. I added a SQLite named
volume so data survives container restarts, and a health check so orchestrators know
when the container is actually ready. This is the same image format that goes into
AWS Lambda in the next step."
Failure modes to handle
| Failure | Where | How to handle |
|---|---|---|
/data volume not writable |
Container startup | Non-root user owns /data via chown in Dockerfile |
| Health check fails on slow startup | docker-compose.yml | start_period: 10s gives app time before health checks count |
.env not found |
docker compose up | Print clear error: "Copy .env.example to .env and fill in LLM_API_KEY" |
| Port 8000 already in use | docker compose up | ports: "8001:8000" as alternative — documented in README |
The metric this project measures
Image size — check with docker images chat-api. Target: under 500MB with multi-stage build.
Container startup time — time from docker compose up to first healthy check. Target: under 15 seconds.
Both are printed by scripts/verify.sh.
Model source currency
- Anthropic model list: https://docs.anthropic.com/en/docs/about-claude/models/overview
- OpenAI model list: https://platform.openai.com/docs/models
- verifiedOn: 2026-08-07
LLM_MODELis required and has no implicit default; choose a supported model for the configured provider at setup time.
The quiz isn't written yet
The project itself is ready to build — the repo, the spec and the deployment steps are all live. What's missing is the written quiz that goes with it, and the order those get written in is decided by which ones people actually ask for.
No spam. Unsubscribe anytime. Replies go to a real person.
The assignment isn't written yet
The project itself is ready to build — the repo, the spec and the deployment steps are all live. What's missing is the written assignment that goes with it, and the order those get written in is decided by which ones people actually ask for.
No spam. Unsubscribe anytime. Replies go to a real person.