---
title: "Dockerize Your App"
description: "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..."
source: "https://confidentprep.com/paths/ai-engineering-fundamentals/dockerize-llm-app/"
path: "AI Engineering Fundamentals"
repository: "https://github.com/confidentprep14-web/ai-engineering-projects/tree/main/dockerize-llm-app"
token_estimate: 3067
---

# Dockerize Your App

## Overview

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.

## What to do

**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.**

```bash
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 up` starts the API and health check passes
- [ ] SQLite data persists to a named volume (survives `docker compose restart`)
- [ ] `docker compose down` stops cleanly
- [ ] Health check in docker-compose.yml marks container unhealthy after 3 failures
- [ ] README includes exactly what to set in `.env` before 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

```bash
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

```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

```yaml
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`, `/stats`
- `src/llm.py` — streaming provider router
- `src/database.py` — SQLite init and logging
- `src/rate_limiter.py` — sliding window rate limiter
- `src/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)

```bash
#!/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

```markdown
# 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

```bash
## 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
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_MODEL` is required and has no implicit default; choose a supported model for the configured provider at setup time.

## Source code

A reference implementation of Dockerize Your App — README, guide, source and tests — is at https://github.com/confidentprep14-web/ai-engineering-projects/tree/main/dockerize-llm-app.
