Estimated time: 2–4 hours · Foundation for Chapter Project
This project upgrades the Project 7 REST API into a fully containerised application backed by PostgreSQL and managed with Alembic migrations. When you finish, your AI backend will run via a single docker compose up command and persist conversation history across restarts.
This is the stack that Chapter 3’s final project (P009) and every subsequent chapter project build on. Getting it solid here means the rest of the chapter is clean.
Your Compose stack must include exactly three services:
| Service | Image | Purpose |
|---|---|---|
api |
Built from your Dockerfile | FastAPI AI backend |
db |
postgres:16-alpine |
PostgreSQL database |
redis |
redis:7-alpine |
Redis cache (used in P009 for Celery) |
Include Redis now even though Celery isn’t added until Lesson 24. Getting the service wired up now means the P009 upgrade is smaller.
All endpoints from Project 7 must continue to work. Add these conversation endpoints:
| Method | Path | Description |
|---|---|---|
| POST | /conversations |
Create a new conversation |
| GET | /conversations |
List conversations (most recent first, limit 20) |
| GET | /conversations/{id} |
Get a conversation with all its messages |
| POST | /conversations/{id}/messages |
Send a message; LLM replies using full conversation history |
| DELETE | /conversations/{id} |
Delete a conversation and all its messages |
Implement the conversations and messages tables from Lesson 22, plus the cost_usd column from Lesson 23. Both tables must be created via Alembic migrations — not create_all.
docker-compose.ymldb service has a PostgreSQL health check; api depends on db with condition: service_healthypostgres_data volume so database survives docker compose down (not docker compose down -v)alembic upgrade head before starting uvicornOPENAI_API_KEY and DATABASE_URL read from .env file via Compose env_filePOST /conversations/{id}/messages passes the full message history to the LLM, not just the latest messageON DELETE CASCADE)docker compose up -d starts all three services with no errorsGET /health returns 200 after containers startalembic current shows head revision on first startupPOST /conversations → POST /conversations/{id}/messages x3 → GET /conversations/{id} returns all 4 messages (3 user, 3 assistant)docker compose down then docker compose up -d — conversations from before the restart are still accessibleDELETE /conversations/{id} returns 204 and subsequent GET returns 404.env is in .dockerignore and .gitignore/chat, /summarize, /classify, /chat/batch)services:
api:
build: .
env_file:
- .env # reads OPENAI_API_KEY, DATABASE_URL etc from .env
environment:
- DATABASE_URL=postgresql+asyncpg://postgres:password@db:5432/aidb
# Note: env_file values + environment values are merged
# environment block takes precedence for the same key
Your app uses the asyncpg URL (postgresql+asyncpg://...).
Alembic needs a sync URL (postgresql+psycopg2://...).
Use a second env var:
# .env
DATABASE_URL=postgresql+asyncpg://postgres:password@db:5432/aidb
ALEMBIC_DATABASE_URL=postgresql+psycopg2://postgres:password@db:5432/aidb
In alembic/env.py, read ALEMBIC_DATABASE_URL.
Even with condition: service_healthy, the application container may start before Postgres accepts connections (the health check only runs every 5s). A simple wait loop in the entrypoint is safer:
#!/bin/bash
set -e
echo "Waiting for database..."
until alembic current > /dev/null 2>&1; do
sleep 1
done
echo "Running migrations..."
alembic upgrade head
echo "Starting server..."
exec uvicorn main:app --host 0.0.0.0 --port 8000
Conversation title auto-generation: When the first message in a conversation is sent, make a second LLM call to generate a short title (5 words or fewer) and update the conversation record. Return the title in the POST /conversations/{id}/messages response.
Token budget enforcement: Add a max_context_tokens field to Conversation. When building the history for an LLM call, trim the oldest messages until the estimated token count is below the budget. (Estimate: 1 token ≈ 4 characters.)
Cost tracking: Populate cost_usd on every message using the token counts from the OpenAI response and the current model pricing. Add a GET /conversations/{id}/cost endpoint that returns total cost for the conversation.
Finished this project? Share your GitHub repo link in the discussion below. Browse what others built to get inspiration and give feedback.