Module 2: Local & Container Deployment
2. Docker Compose for AI Apps
Overview
In this capsule you'll build a complete Docker Compose file for a multi-container AI app. Not a "hello world" with one service — a real system with FastAPI, Redis, and the structure a production AI app needs. By the end, you'll have a functional Compose you can adapt to any AI project.
Context: Docker Compose defines your infrastructure as code: what services run, how they communicate, what data persists. It's the "blueprint" of your system. In this guide, the Compose you build here is the base on which LocalStack (M4), migration patterns (M6), and the capstone project (M8) integrate.
Compose File: Complete Anatomy
Basic structure
# docker-compose.yml
# Each service is a container with its configuration
services:
api:
# Your FastAPI app — the entry point
build: ./api
ports:
- "8000:8000"
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
- REDIS_URL=redis://cache:6379
depends_on:
cache:
condition: service_healthy
cache:
# Redis — response cache
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
volumes:
redis_data:
Each section explained
services: # Defines the containers that make up your system
api: # Service name (also its hostname on the internal network)
build: ./api # Builds from the Dockerfile in ./api/
ports: # host:container mapping
environment: # Environment variables
depends_on: # Which services it needs before starting
cache:
image: redis:7-alpine # Uses a pre-built image (doesn't build)
volumes: # Persistent data
healthcheck: # Health verification
volumes: # Definition of persistent volumes
redis_data: # Redis data persists between restarts
The AI App: Complete Code
FastAPI with Redis cache
# api/main.py
import hashlib
import json
import os
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from openai import OpenAI
import redis
app = FastAPI(title="AI API with Cache")
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
cache = redis.Redis.from_url(
os.environ.get("REDIS_URL", "redis://localhost:6379"),
decode_responses=True
)
class AskRequest(BaseModel):
prompt: str
max_tokens: int = 500
use_cache: bool = True
class AskResponse(BaseModel):
answer: str
cached: bool
tokens_used: int | None = None
@app.get("/health")
def health():
redis_ok = False
try:
redis_ok = cache.ping()
except redis.ConnectionError:
pass
return {
"status": "healthy" if redis_ok else "degraded",
"services": {
"api": "up",
"redis": "up" if redis_ok else "down",
}
}
@app.post("/ask", response_model=AskResponse)
def ask(request: AskRequest):
cache_key = hashlib.md5(
f"{request.prompt}:{request.max_tokens}".encode()
).hexdigest()
if request.use_cache:
cached_response = cache.get(cache_key)
if cached_response:
data = json.loads(cached_response)
return AskResponse(answer=data["answer"], cached=True)
try:
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": request.prompt}],
max_tokens=request.max_tokens,
)
except Exception as e:
raise HTTPException(status_code=502, detail=f"LLM API error: {str(e)}")
answer = response.choices[0].message.content
tokens = response.usage.total_tokens
cache.setex(
cache_key,
3600, # TTL: 1 hour
json.dumps({"answer": answer, "tokens": tokens})
)
return AskResponse(answer=answer, cached=False, tokens_used=tokens)
Dockerfile for the API
# api/Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Requirements
# api/requirements.txt
fastapi==0.115.0
uvicorn==0.30.0
openai>=1.0.0
redis==5.0.0
pydantic>=2.0.0
Complete Compose: 3 Services
# docker-compose.yml
services:
api:
build:
context: ./api
dockerfile: Dockerfile
ports:
- "8000:8000"
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
- REDIS_URL=redis://cache:6379
- ENVIRONMENT=${ENVIRONMENT:-development}
depends_on:
cache:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
restart: unless-stopped
cache:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
restart: unless-stopped
redis-commander:
image: rediscommander/redis-commander:latest
environment:
- REDIS_HOSTS=local:cache:6379
ports:
- "8081:8081"
depends_on:
cache:
condition: service_healthy
profiles:
- debug
volumes:
redis_data:
Bring it up and test
# Bring up the main services
docker compose up -d
# Expected output:
# ✔ Network module-02_default Created
# ✔ Container module-02-cache-1 Healthy
# ✔ Container module-02-api-1 Started
# Verify health
curl http://localhost:8000/health
# {"status":"healthy","services":{"api":"up","redis":"up"}}
# Test the ask endpoint
curl -X POST http://localhost:8000/ask \
-H "Content-Type: application/json" \
-d '{"prompt": "What is Docker Compose in one line?"}'
# {"answer":"Docker Compose is...","cached":false,"tokens_used":45}
# Second request (cached)
curl -X POST http://localhost:8000/ask \
-H "Content-Type: application/json" \
-d '{"prompt": "What is Docker Compose in one line?"}'
# {"answer":"Docker Compose is...","cached":true,"tokens_used":null}
# Bring up with debug tools
docker compose --profile debug up -d
# Now redis-commander is available at http://localhost:8081
Common Compose Patterns for AI
Pattern 1: API + Cache
The most basic and most common. FastAPI + Redis. Covers 70% of AI apps.
Pattern 2: API + Cache + Vector Store
For RAG apps that need semantic search:
services:
api:
build: ./api
ports:
- "8000:8000"
depends_on:
cache:
condition: service_healthy
vectordb:
condition: service_healthy
cache:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
vectordb:
image: qdrant/qdrant:latest
ports:
- "6333:6333"
volumes:
- qdrant_data:/qdrant/storage
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:6333/healthz"]
interval: 10s
volumes:
qdrant_data:
Pattern 3: API + Cache + Worker (async)
For processing that doesn't fit in the request/response cycle (long documents, embedding batches):
services:
api:
build: ./api
ports:
- "8000:8000"
environment:
- REDIS_URL=redis://cache:6379
cache:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
worker:
build: ./worker
environment:
- REDIS_URL=redis://cache:6379
- OPENAI_API_KEY=${OPENAI_API_KEY}
depends_on:
cache:
condition: service_healthy
# The worker reads jobs from Redis and processes them in the background
# The API publishes jobs, the worker consumes them
Pattern comparison
| Pattern | Services | When to use it | Complexity |
|---|---|---|---|
| API + Cache | 2 | Chatbot, Q&A, assistants | Low |
| API + Cache + VectorDB | 3 | RAG, semantic search | Medium |
| API + Cache + Worker | 3 | Document processing, batch | Medium |
| API + Cache + VectorDB + Worker | 4+ | Complete AI system | High |
Recommendation: Start with Pattern 1 (API + Cache). Add services when you need them, not before. The Compose file is easy to extend — easier than refactoring a monolith.
Advanced pattern: With Nginx as a reverse proxy
For production or when you scale the API:
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
api:
condition: service_healthy
api:
build: ./api
expose:
- "8000" # Only visible internally
# ...rest of the config
This prepares your Compose for capsule 05 (Networking), where you'll go deep on reverse proxy, SSL, and isolated networks.
Comparison: Docker Run vs Docker Compose
| Aspect | docker run | docker compose |
|---|---|---|
| Services | 1 container | N containers |
| Networking | Manual (--network) | Automatic |
| Volumes | Manual (-v) | Declarative |
| Dependencies | Not managed | depends_on + healthcheck |
| Reproducibility | Long commands | One YAML file |
| Scaling | Manual | docker compose up --scale api=3 |
Troubleshooting
Problem 1: "The api service doesn't connect to Redis"
# Verify that Redis is running
docker compose ps
# cache should be "Up (healthy)"
# Verify networking
docker compose exec api ping cache
# It should resolve to Redis's internal IP
# Verify the environment variable
docker compose exec api env | grep REDIS
# REDIS_URL=redis://cache:6379
Problem 2: "The build takes too long"
# Optimize the Dockerfile with layer caching
# BEFORE (rebuild everything when code changes):
COPY . .
RUN pip install -r requirements.txt
# AFTER (only rebuild if requirements change):
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
Problem 3: "The OpenAI API key doesn't reach the container"
# Verify that the variable is defined in your shell
echo $OPENAI_API_KEY
# If it's empty, Compose passes an empty variable to the container
# Verify that .env exists and has the variable
cat .env | grep OPENAI
# OPENAI_API_KEY=sk-proj-...
# Verify inside the container
docker compose exec api env | grep OPENAI
# If it doesn't appear, check that your docker-compose.yml has:
# environment:
# - OPENAI_API_KEY=${OPENAI_API_KEY}
Problem 4: "Redis data is lost on restart"
Make sure you have a volume defined and appendonly yes:
cache:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data # Persistence
volumes:
redis_data: # Must be declared
Problem 5: "Container restarts in a loop (restart: always)"
# Check the restart count
docker compose ps
# If you see "Restarting (1)" repeatedly:
# 1. See the crash logs
docker compose logs api --tail=50
# Look for the error causing the exit
# 2. Temporarily disable restart to see the error
# Change restart: unless-stopped → restart: "no"
# Bring it up again and read the full error log
# 3. Common causes in AI apps:
# - Empty OPENAI_API_KEY → the app validates and fails
# - Redis not ready → ConnectionRefusedError on startup
# - Port already in use → bind: address already in use
Hands-On Exercises
Exercise 1: Add a health monitoring service
Add a service that every 60 seconds curls the API's /health endpoint and logs the result.
See solution
health-monitor:
image: alpine/curl
command: >
sh -c "while true; do
echo \"$(date): $(curl -s http://api:8000/health)\";
sleep 60;
done"
depends_on:
api:
condition: service_healthy
Exercise 2: Add Qdrant as a vector store
Extend the Compose to include Qdrant and make the API depend on it.
See solution
vectordb:
image: qdrant/qdrant:latest
ports:
- "6333:6333"
volumes:
- qdrant_data:/qdrant/storage
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:6333/healthz"]
interval: 10s
timeout: 5s
retries: 3
# Add to api:
api:
depends_on:
cache:
condition: service_healthy
vectordb:
condition: service_healthy
environment:
- QDRANT_URL=http://vectordb:6333
# Add at the end:
volumes:
redis_data:
qdrant_data:
Exercise 3: Scaling the API
Bring up 3 instances of the API and verify that they're all healthy.
See solution
# First, remove the fixed port mapping (you can't have 3 on port 8000)
# In docker-compose.yml, change ports to expose:
# api:
# expose:
# - "8000"
# # Remove ports: - "8000:8000"
# Then scale
docker compose up -d --scale api=3
# Verify
docker compose ps
# It should show 3 instances of api, all healthy
# To access them, you need a load balancer (Nginx) in front
# That's advanced — for now, verify that all 3 start
Exercise 4: Docker Compose with profiles
Create a "dev" profile that includes redis-commander and hot reload, and a "prod" profile that doesn't include them.
See solution
services:
api:
build: ./api
# In dev: hot reload with volume mount
volumes:
- ./api:/app # Dev only
profiles:
- dev
- prod
api-prod:
build: ./api
# No volume mount, no reload
profiles:
- prod
cache:
image: redis:7-alpine
# Always present (no profile = all profiles)
redis-commander:
image: rediscommander/redis-commander:latest
profiles:
- dev # Dev only
docker compose --profile dev up -d # Dev with debug tools
docker compose --profile prod up -d # Prod without extras
Summary
- Docker Compose defines your infrastructure as code: services, networks, volumes.
- For AI apps, the base pattern is FastAPI + Redis (response cache).
depends_onwithcondition: service_healthyensures services start in order.- Volumes persist data between restarts (Redis data, vector stores).
- Profiles separate dev and prod configuration.
- The Compose you build here is the base for M4, M6, and M8.
Additional Resources
- Docker Compose Specification — Official format reference
- Compose Deploy Specification — Deployment config
- Redis Docker Official Image — Redis image documentation
- Qdrant Docker — Qdrant on Docker
- FastAPI with Docker — Deploy FastAPI on Docker
- Docker Compose Networking — Networking between services