Module 2: Local & Container Deployment

8. Project: Local Multi-Container AI App

Project Overview

This is the capstone project of Module 2. You'll build a complete multi-container AI app with Docker Compose: FastAPI as the API gateway, Redis as the response cache, per-environment configuration, functional health checks, and a documented debugging flow. The result is a local system that replicates production.

Why it matters: This Compose is the base artifact for the rest of the guide. In Module 4, you'll add LocalStack as a service. In Module 6, it'll be your development environment while you migrate to AWS. And in Module 8, it'll be the starting point of the Docker → CI/CD → platform pipeline.


Project Objective

Produce a functional Docker Compose that:

  1. Brings up a multi-container AI app with docker compose up
  2. Includes FastAPI (API), Redis (cache), and health checks
  3. Has per-environment configuration (dev, staging, prod)
  4. Handles secrets safely
  5. Caches LLM responses to reduce costs
  6. Can be debugged with the module's systematic flow

Module Recap

CapsuleConceptYou use it in the project
02Docker Compose multi-serviceBase structure of the project
03Environment configuration.env files, overrides
04Health checks and dependenciesVerified readiness
05Networking and communicationConnected services
06DebuggingDiagnostic flow
07Secrets and securityProtected API keys

Technical Specifications

Architecture

                    ┌─────────────┐
    HTTP :8000 ───→ │   FastAPI    │
                    │   (api)      │
                    └──────┬──────┘
                           │
                    ┌──────┴──────┐
                    │             │
              ┌─────┴─────┐ ┌────┴─────┐
              │   Redis    │ │  OpenAI  │
              │  (cache)   │ │   API    │
              └───────────┘ └──────────┘

Required services

ServiceImage/BuildPortHealth check
apiBuild from ./api8000 (host)GET /health
cacheredis:7-alpine6379 (internal)redis-cli ping
redis-commanderrediscommander/redis-commander8081 (debug only)profile: debug

Required endpoints

GET  /health              → Status of all services
GET  /health/detailed     → Latency of each dependency
POST /ask                 → Ask the LLM (with cache)
GET  /cache/stats         → Cache hit rate
DELETE /cache             → Clear the cache

Detailed requirements

API (FastAPI):

  • Must start only when Redis is healthy (depends_on with condition: service_healthy)
  • The /ask endpoint must try the cache first, calling OpenAI only on a cache miss
  • If Redis is down, /ask must keep working (graceful degradation) — call OpenAI directly
  • The health check must report the individual status of each dependency
  • OpenAI API errors must return HTTP 502 with a descriptive message
  • Logging configured by environment variable (LOG_LEVEL)

Cache (Redis):

  • Configured with maxmemory and the allkeys-lru policy (so it doesn't grow without limit)
  • Data persisted with appendonly yes
  • Configurable TTL via environment variable (CACHE_TTL)

Configuration:

  • All sensitive variables in .env (never in code)
  • .env.example with placeholders for onboarding
  • docker-compose.override.yml for development (hot reload, debug logging)
  • docker-compose.prod.yml for production (multiple workers, warning logging, resource limits)
  • Pydantic Settings validates that OPENAI_API_KEY exists and isn't a placeholder

Debugging:

  • A debug.sh script that runs a 5-step diagnosis
  • redis-commander available with the debug profile

Required files

module-02-project/
├── api/
│   ├── main.py                  # Complete FastAPI app
│   ├── config.py                # Settings with Pydantic
│   ├── requirements.txt         # Dependencies
│   └── Dockerfile               # Multi-stage or slim
├── docker-compose.yml           # Base config
├── docker-compose.override.yml  # Dev overrides (hot reload)
├── docker-compose.prod.yml      # Production overrides
├── .env                         # Dev variables (NOT in Git)
├── .env.example                 # Template (DO put in Git)
├── .gitignore                   # Ignores .env and secrets
├── .dockerignore                # Ignores .env in the build
└── debug.sh                     # Diagnostic script

Evaluation Rubric (100 points)

Functionality (40 points)

CriterionPointsHow it's evaluated
docker compose up brings up all services without errors8Run and verify docker compose ps
/health returns the correct status of api and redis6curl /health shows both services
/health/detailed shows dependency latency4curl /health/detailed includes latency_ms
/ask invokes the LLM and returns a response8POST with a prompt returns answer
Cache works: an identical second request returns cached: true8Repeat the request, verify the cached field
/cache/stats shows hit rate and memory3curl /cache/stats returns metrics
DELETE /cache clears the cache3DELETE + repeat request = cached: false

Configuration (25 points)

CriterionPointsHow it's evaluated
.env is not in Git; .env.example is5git status, verify .gitignore
.dockerignore excludes .env and sensitive files3cat .dockerignore
Pydantic Settings validates OPENAI_API_KEY5Start without the key → clear error
docker-compose.override.yml with hot reload4Edit code → reflected without a rebuild
docker-compose.prod.yml with workers and resource limits4docker compose -f ... config shows workers
Configurable environment variables (LOG_LEVEL, CACHE_TTL, etc.)4Change in .env, restart, verify behavior

Debugging (20 points)

CriterionPointsHow it's evaluated
debug.sh runs the 5 diagnostic steps8bash debug.sh produces useful output
Functional health checks (api and cache)4docker compose ps shows (healthy)
Graceful degradation: app works without Redis4docker compose stop cache, then /ask keeps working
redis-commander accessible with the debug profile4docker compose --profile debug up -d, open :8081

Documentation (15 points)

CriterionPointsHow it's evaluated
.env.example complete with all variables5Compare variables in code vs .env.example
Clear setup instructions (README or comments)5Someone new can bring up the project following the instructions
Commented code where it's not obvious (config, health checks)5Read the code, understand without external explanation

Grading scale

RangeGrade
90-100Excellent — production-ready
75-89Good — functional with minor improvements
60-74Acceptable — works but lacks robustness
< 60Needs work — review the module capsules

Minimal Implementation Example

This is the minimal example that passes the rubric (≥60 points). Your implementation should be better than this.

api/config.py (minimal)

import os
from pydantic_settings import BaseSettings
from pydantic import field_validator

class Settings(BaseSettings):
    openai_api_key: str
    redis_url: str = "redis://cache:6379"
    environment: str = "development"
    log_level: str = "debug"
    cache_ttl: int = 3600
    model_name: str = "gpt-4o-mini"
    max_tokens: int = 500

    @field_validator("openai_api_key")
    @classmethod
    def validate_key(cls, v):
        if not v or "replace" in v.lower():
            raise ValueError("Set a real OPENAI_API_KEY in .env")
        return v

    class Config:
        env_file = ".env"

settings = Settings()

api/main.py (minimal)

import hashlib
import json
import logging
import time
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from openai import OpenAI
import redis

from config import settings

logging.basicConfig(level=getattr(logging, settings.log_level.upper()))
logger = logging.getLogger(__name__)

app = FastAPI(title="AI API", version="1.0.0")
client = OpenAI(api_key=settings.openai_api_key, timeout=30.0)
cache = redis.Redis.from_url(settings.redis_url, 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
    model: str = ""

@app.get("/health")
def health():
    checks = {"api": "up"}
    try:
        cache.ping()
        checks["redis"] = "up"
    except Exception:
        checks["redis"] = "down"

    overall = "healthy" if all(v == "up" for v in checks.values()) else "degraded"
    status_code = 200 if overall == "healthy" else 503
    return JSONResponse(
        status_code=status_code,
        content={"status": overall, "environment": settings.environment, "services": checks}
    )

@app.get("/health/detailed")
def health_detailed():
    checks = {}

    start = time.time()
    try:
        cache.ping()
        checks["redis"] = {"status": "up", "latency_ms": round((time.time() - start) * 1000, 1)}
    except Exception as e:
        checks["redis"] = {"status": "down", "error": str(e)}

    start = time.time()
    try:
        client.chat.completions.create(
            model=settings.model_name,
            messages=[{"role": "user", "content": "ping"}],
            max_tokens=5
        )
        checks["openai"] = {"status": "up", "latency_ms": round((time.time() - start) * 1000, 1)}
    except Exception as e:
        checks["openai"] = {"status": "down", "error": str(e)}

    overall = "healthy" if all(c.get("status") == "up" for c in checks.values()) else "degraded"
    return {"status": overall, "checks": checks}

@app.post("/ask", response_model=AskResponse)
def ask(request: AskRequest):
    cache_key = f"ask:{hashlib.md5(f'{request.prompt}:{request.max_tokens}'.encode()).hexdigest()}"

    if request.use_cache:
        try:
            cached = cache.get(cache_key)
            if cached:
                data = json.loads(cached)
                return AskResponse(answer=data["answer"], cached=True, model=data.get("model", ""))
        except redis.ConnectionError:
            logger.warning("Redis unavailable, proceeding without cache")

    try:
        response = client.chat.completions.create(
            model=settings.model_name,
            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

    try:
        cache.setex(
            cache_key, settings.cache_ttl,
            json.dumps({"answer": answer, "tokens": tokens, "model": settings.model_name})
        )
    except redis.ConnectionError:
        logger.warning("Redis unavailable, response not cached")

    return AskResponse(answer=answer, cached=False, tokens_used=tokens, model=settings.model_name)

@app.get("/cache/stats")
def cache_stats():
    try:
        info = cache.info()
        return {
            "hits": info.get("keyspace_hits", 0),
            "misses": info.get("keyspace_misses", 0),
            "hit_rate": round(
                info.get("keyspace_hits", 0) / max(1, info.get("keyspace_hits", 0) + info.get("keyspace_misses", 0)) * 100, 1
            ),
            "keys": cache.dbsize(),
            "memory_used": info.get("used_memory_human", "unknown"),
        }
    except redis.ConnectionError:
        return {"error": "Redis unavailable"}

@app.delete("/cache")
def clear_cache():
    try:
        cache.flushdb()
        return {"status": "cache cleared"}
    except redis.ConnectionError:
        raise HTTPException(status_code=503, detail="Redis unavailable")

api/requirements.txt

fastapi==0.115.0
uvicorn==0.30.0
openai>=1.0.0
redis==5.0.0
pydantic>=2.0.0
pydantic-settings>=2.0.0

api/Dockerfile

FROM python:3.11-slim

RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

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"]

Complete Code (Reference Implementation)

This reference implementation includes everything needed to score 90+ points.

api/config.py

import os
from pydantic_settings import BaseSettings
from pydantic import field_validator

class Settings(BaseSettings):
    openai_api_key: str
    redis_url: str = "redis://cache:6379"
    environment: str = "development"
    log_level: str = "debug"
    cache_ttl: int = 3600
    model_name: str = "gpt-4o-mini"
    max_tokens: int = 500

    @field_validator("openai_api_key")
    @classmethod
    def validate_key(cls, v):
        if not v or "replace" in v.lower():
            raise ValueError("Set a real OPENAI_API_KEY in .env")
        return v

    @field_validator("log_level")
    @classmethod
    def validate_log_level(cls, v):
        valid = {"debug", "info", "warning", "error", "critical"}
        if v.lower() not in valid:
            raise ValueError(f"LOG_LEVEL must be one of: {valid}")
        return v.lower()

    @property
    def is_dev(self) -> bool:
        return self.environment == "development"

    class Config:
        env_file = ".env"

settings = Settings()

api/main.py

import hashlib
import json
import logging
import time
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from openai import OpenAI
import redis

from config import settings

logging.basicConfig(level=getattr(logging, settings.log_level.upper()))
logger = logging.getLogger(__name__)

client = OpenAI(api_key=settings.openai_api_key, timeout=30.0, max_retries=2)
cache = redis.Redis.from_url(settings.redis_url, decode_responses=True)

@asynccontextmanager
async def lifespan(app: FastAPI):
    try:
        cache.ping()
        logger.info("Redis connection established")
    except redis.ConnectionError:
        logger.warning("Redis not available at startup — will retry on requests")
    yield

app = FastAPI(title="AI API", version="1.0.0", lifespan=lifespan)

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
    model: str = ""

@app.get("/health")
def health():
    checks = {"api": "up"}
    try:
        cache.ping()
        checks["redis"] = "up"
    except Exception:
        checks["redis"] = "down"

    overall = "healthy" if all(v == "up" for v in checks.values()) else "degraded"
    status_code = 200 if overall == "healthy" else 503
    return JSONResponse(
        status_code=status_code,
        content={"status": overall, "environment": settings.environment, "services": checks}
    )

@app.get("/health/detailed")
def health_detailed():
    checks = {}

    start = time.time()
    try:
        cache.ping()
        checks["redis"] = {"status": "up", "latency_ms": round((time.time() - start) * 1000, 1)}
    except Exception as e:
        checks["redis"] = {"status": "down", "error": str(e)}

    start = time.time()
    try:
        r = client.chat.completions.create(
            model=settings.model_name,
            messages=[{"role": "user", "content": "ping"}],
            max_tokens=5
        )
        checks["openai"] = {"status": "up", "latency_ms": round((time.time() - start) * 1000, 1)}
    except Exception as e:
        checks["openai"] = {"status": "down", "error": str(e)}

    overall = "healthy" if all(c.get("status") == "up" for c in checks.values()) else "degraded"
    return {"status": overall, "checks": checks}

@app.post("/ask", response_model=AskResponse)
def ask(request: AskRequest):
    cache_key = f"ask:{hashlib.md5(f'{request.prompt}:{request.max_tokens}'.encode()).hexdigest()}"

    if request.use_cache:
        try:
            cached = cache.get(cache_key)
            if cached:
                data = json.loads(cached)
                logger.debug(f"Cache hit for key {cache_key[:8]}")
                return AskResponse(answer=data["answer"], cached=True, model=data.get("model", ""))
        except redis.ConnectionError:
            logger.warning("Redis unavailable, proceeding without cache")

    try:
        response = client.chat.completions.create(
            model=settings.model_name,
            messages=[{"role": "user", "content": request.prompt}],
            max_tokens=request.max_tokens,
        )
    except Exception as e:
        logger.error(f"OpenAI API error: {e}")
        raise HTTPException(status_code=502, detail=f"LLM API error: {str(e)}")

    answer = response.choices[0].message.content
    tokens = response.usage.total_tokens

    try:
        cache.setex(
            cache_key, settings.cache_ttl,
            json.dumps({"answer": answer, "tokens": tokens, "model": settings.model_name})
        )
    except redis.ConnectionError:
        logger.warning("Redis unavailable, response not cached")

    return AskResponse(answer=answer, cached=False, tokens_used=tokens, model=settings.model_name)

@app.get("/cache/stats")
def cache_stats():
    try:
        info = cache.info()
        return {
            "hits": info.get("keyspace_hits", 0),
            "misses": info.get("keyspace_misses", 0),
            "hit_rate": round(
                info.get("keyspace_hits", 0) / max(1, info.get("keyspace_hits", 0) + info.get("keyspace_misses", 0)) * 100, 1
            ),
            "keys": cache.dbsize(),
            "memory_used": info.get("used_memory_human", "unknown"),
        }
    except redis.ConnectionError:
        return {"error": "Redis unavailable"}

@app.delete("/cache")
def clear_cache():
    try:
        cache.flushdb()
        return {"status": "cache cleared"}
    except redis.ConnectionError:
        raise HTTPException(status_code=503, detail="Redis unavailable")

api/requirements.txt

fastapi==0.115.0
uvicorn==0.30.0
openai>=1.0.0
redis==5.0.0
pydantic>=2.0.0
pydantic-settings>=2.0.0

api/Dockerfile

FROM python:3.11-slim

RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

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"]

docker-compose.yml

services:
  api:
    build:
      context: ./api
    ports:
      - "${API_PORT:-8000}:8000"
    env_file:
      - .env
    environment:
      - REDIS_URL=redis://cache:6379
    depends_on:
      cache:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 15s
    restart: unless-stopped

  cache:
    image: redis:7-alpine
    command: redis-server --appendonly yes --maxmemory 128mb --maxmemory-policy allkeys-lru
    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:

docker-compose.override.yml (dev)

services:
  api:
    volumes:
      - ./api:/app
    command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
    environment:
      - LOG_LEVEL=debug

docker-compose.prod.yml

services:
  api:
    command: uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4
    environment:
      - LOG_LEVEL=warning
      - ENVIRONMENT=production
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "1.0"
        reservations:
          memory: 256M

.env.example

OPENAI_API_KEY=sk-proj-replace-with-your-key
ENVIRONMENT=development
LOG_LEVEL=debug
CACHE_TTL=3600
MODEL_NAME=gpt-4o-mini
MAX_TOKENS=500
API_PORT=8000

.gitignore

.env
.env.production
.env.staging
!.env.example
secrets/
__pycache__/
*.pyc

.dockerignore

.env
.env.*
!.env.example
secrets/
.git/
__pycache__/
*.pyc
.gitignore
debug.sh
docker-compose*.yml

debug.sh

#!/bin/bash
echo "=== Service Status ==="
docker compose ps

echo -e "\n=== Health Check ==="
curl -s http://localhost:8000/health | python3 -m json.tool 2>/dev/null || echo "API not responding"

echo -e "\n=== Detailed Health ==="
curl -s http://localhost:8000/health/detailed | python3 -m json.tool 2>/dev/null || echo "Detailed health unavailable"

echo -e "\n=== Cache Stats ==="
curl -s http://localhost:8000/cache/stats | python3 -m json.tool 2>/dev/null || echo "Stats unavailable"

echo -e "\n=== Resource Usage ==="
docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"

echo -e "\n=== Recent Logs (errors only) ==="
docker compose logs --tail 20 2>&1 | grep -i "error\|exception\|failed" || echo "No errors found"

echo -e "\n=== Config Validation ==="
docker compose config --quiet && echo "✅ Config OK" || echo "❌ Config ERROR"

Step by Step to Build It

1. Create the structure (2 min)

mkdir -p module-02-project/api
cd module-02-project

2. Create the code files (10 min)

Copy the files from the previous sections: config.py, main.py, requirements.txt, Dockerfile.

3. Create the Docker Compose files (5 min)

Copy docker-compose.yml, docker-compose.override.yml, docker-compose.prod.yml.

4. Create the security files (2 min)

Copy .gitignore, .dockerignore, .env.example.

5. Configure the environment (2 min)

cp .env.example .env
# Edit .env with your real OPENAI_API_KEY

6. Bring it up and test (5 min)

docker compose up -d
# Wait for the health checks to pass

# Test health
curl http://localhost:8000/health

# Test ask
curl -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"prompt": "What is Docker Compose?"}'

# Test cache (repeat the same request)
curl -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"prompt": "What is Docker Compose?"}'
# Should return cached: true

# Cache stats
curl http://localhost:8000/cache/stats

7. Test per-environment configuration (3 min)

# Production mode
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
curl http://localhost:8000/health
# environment: "production" if you configured .env.production

8. Test debugging (3 min)

chmod +x debug.sh
bash debug.sh

# Test redis-commander
docker compose --profile debug up -d
# Open http://localhost:8081 in the browser

9. Test graceful degradation (3 min)

# Stop Redis
docker compose stop cache

# Verify that the API still responds
curl http://localhost:8000/health
# {"status":"degraded","services":{"api":"up","redis":"down"}}

curl -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"prompt": "test without cache"}'
# Should work (without cache, straight to OpenAI)

# Restore Redis
docker compose start cache

Completeness Checklist

Functionality

  • docker compose up brings up all services without errors
  • /health returns the status of api and redis
  • /health/detailed shows the latency of each dependency
  • /ask invokes the LLM and returns a response
  • An identical second request returns cached: true
  • /cache/stats shows hit rate and memory
  • DELETE /cache clears the cache correctly

Configuration

  • .env is not in Git; .env.example is
  • .dockerignore excludes .env and sensitive files
  • Settings config validates that OPENAI_API_KEY exists
  • Health checks verify real readiness
  • Compose works with override (dev) and prod

Debugging

  • debug.sh runs without errors and produces useful output
  • redis-commander accessible with --profile debug
  • App works in degraded mode without Redis

Documentation

  • .env.example has all the necessary variables
  • Code has comments where it's not obvious

Project Troubleshooting

"docker compose up fails with 'openai_api_key is required'"

Your .env doesn't have OPENAI_API_KEY or it's a placeholder. Copy .env.example to .env and fill it with your real key.

cp .env.example .env
# Edit .env: OPENAI_API_KEY=sk-proj-your-real-key
docker compose up -d

"The first request is very slow"

Normal in dev with hot reload — uvicorn reloads modules on the first request. In prod (without --reload), it's faster. The API's cold start is independent of Lambda's cold start.

# Verify response time
time curl -s -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"prompt":"ping"}'
# First request: 3-10s (normal, includes OpenAI cold start)
# Following requests (cached): <100ms

"redis-commander doesn't appear"

You need the debug profile: docker compose --profile debug up -d

# Verify that the container is running
docker compose --profile debug ps
# redis-commander should be Up

# Verify the port
curl -s http://localhost:8081 | head -5
# Should return HTML

"Cache doesn't work — always returns cached: false"

# Verify that Redis is healthy
docker compose ps cache
# Should be: Up (healthy)

# Verify connectivity from the API
docker compose exec api python -c "
import redis
r = redis.Redis.from_url('redis://cache:6379', decode_responses=True)
r.set('test', 'hello')
print(r.get('test'))  # Should print: hello
"

# Verify that CACHE_TTL isn't 0
docker compose exec api env | grep CACHE_TTL

"Exit code 137 on heavy requests"

Your container is running out of memory. Increase the limit or reduce usage:

# See current memory usage
docker stats --no-stream

# If you're using docker-compose.prod.yml with memory: 512M
# and your app needs more, increase it:
# deploy.resources.limits.memory: 1G

# Or run without limits for dev:
docker compose up -d  # uses override.yml with no limits

"docker compose build takes too long"

The first build downloads the base image and the dependencies. The following ones are faster thanks to layer caching:

# If you need to force a clean rebuild:
docker compose build --no-cache api

# Tip: separate COPY requirements.txt and pip install BEFORE COPY . .
# That way pip install only re-runs if requirements.txt changed

Connection to the Guide

This Docker Compose is your base artifact. In the coming modules:

  • M3 (Lambda): You'll learn the serverless alternative
  • M4 (LocalStack): You'll add LocalStack as a service in this Compose
  • M6 (Migration): This Compose will be your development environment
  • M8 (Capstone): This Compose is the starting point for the deploy to production

Summary

  • The project integrates all the capsules of Module 2: Compose, env config, health checks, networking, debugging, and secrets.
  • The architecture is FastAPI + Redis + OpenAI API — the base stack for AI apps with cache.
  • Graceful degradation is key: if Redis goes down, the app keeps working (without cache).
  • Per-environment configuration (override.yml for dev, prod.yml for production) reflects how real teams work.
  • Pydantic Settings validates secrets at startup — it never starts with invalid configuration.
  • The debug.sh script runs the 5 diagnostic steps in one command.
  • This Compose is your base artifact for the rest of the guide — you'll reuse it in modules 4, 6, and 8.

Resources for the Project

  1. Docker Compose File Reference — Complete specification
  2. FastAPI Production Deployment — Official deployment guide
  3. Redis Configuration — Redis configuration
  4. Uvicorn Deployment — Uvicorn deployment options
  5. Docker Best Practices — Dockerfile best practices
  6. Pydantic Settings — Configuration validation
  7. OpenAI Python SDK — Official OpenAI client