Shipping Python APIs/
Lesson

Your APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. needs a database. Maybe Redis too. Running docker run with ten flags for each service gets old fast. Docker ComposeWhat is docker compose?A tool that lets you define and run multi-container applications from a single YAML file. One command starts your entire stack. lets you describe your entire development stack in one file and start everything with a single command. But compose files have subtleties that AI consistently gets wrong, especially around service readiness.

The compose file structure

Docker ComposeWhat is docker compose?A tool that lets you define and run multi-container applications from a single YAML file. One command starts your entire stack. uses compose.yaml (the modern name) or docker-compose.yml (the legacy name, still supported). Here is a development setup for a FastAPI app with PostgreSQL:

yaml
# compose.yaml
services:
  app:
    build: .
    ports:
      - "8000:8000"
    volumes:
      - ./src:/app/src
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/myapp
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: myapp
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5

volumes:
  pgdata:

One file. One docker compose up. Your APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. and database are running, connected, and the database is confirmed ready before your app starts.

02

Service definitions

Each entry under services defines a containerWhat is container?A lightweight, portable package that bundles your application code with all its dependencies so it runs identically on any machine.. A service can be built from a Dockerfile (build: .) or pulled from a registryWhat is registry?A server that stores and distributes packages or container images - npm registry for JavaScript packages, Docker Hub for container images. (image: postgres:16-alpine).

Build vs image

yaml
services:
  # Built from your Dockerfile
  app:
    build: .

  # Pulled from Docker Hub
  db:
    image: postgres:16-alpine

For your own application, use build. For infrastructure services (databases, caches, message queues), use image with a pinned version. Never use image: postgres:latest, version jumps in databases can corrupt data.

Port mapping

yaml
ports:
  - "8000:8000"    # host:container

The left side is the port on your machine. The right side is the port inside the container. You can map different ports: "3000:8000" means you access the app on localhost:3000 while it listens on port 8000 inside the container.

Environment variables

Two syntaxes, same result:

yaml
# List format
environment:
  - DATABASE_URL=postgresql://postgres:postgres@db:5432/myapp
  - DEBUG=true

# Map format
environment:
  DATABASE_URL: postgresql://postgres:postgres@db:5432/myapp
  DEBUG: "true"

For sensitive values, use an .env file:

yaml
env_file:
  - .env

Make sure .env is in your .gitignore and .dockerignore.

03

Volumes for live reloading

Without volumes, changing code means rebuilding the image. With bind mounts, your local files are synced into the containerWhat is container?A lightweight, portable package that bundles your application code with all its dependencies so it runs identically on any machine. in real time.

yaml
volumes:
  - ./src:/app/src       # your code → container
  - /app/__pycache__     # anonymous volume: keep container's cache

The first line mounts your src directory into /app/src inside the container. When you edit a file, uvicorn (with --reload) detects the change and restarts automatically.

The second line is an anonymous volume that prevents the container's __pycache__ from being overwritten by (or leaking to) your host. This is the same pattern Node.js projects use with /app/node_modules.

For development, you also want uvicorn to watch for changes:

yaml
services:
  app:
    build: .
    command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
    volumes:
      - ./src:/app/src

The command override replaces the CMD from your Dockerfile. In production, you remove this override and use the Dockerfile's CMD.

04

The depends_on trap

This is the most common bug in AI-generated compose files.

yaml
# What AI generates
services:
  app:
    depends_on:
      - db
  db:
    image: postgres:16-alpine

depends_on: [db] means: start the db containerWhat is container?A lightweight, portable package that bundles your application code with all its dependencies so it runs identically on any machine. before the app container. It does not mean: wait until PostgreSQL is actually accepting connections. The db container starts, PostgreSQL begins its initialization (creating system tables, loading configs), and a few seconds later the app container starts and tries to connect. PostgreSQL is not ready yet. Your app crashes.

AI pitfall
Every AI-generated compose file uses bare depends_on without health checks. The AI has no concept of startup time. It sees that depends_on exists and assumes it handles readiness. It does not. Your app will crash intermittently on startup, and the bug is maddening because sometimes the database initializes fast enough and it works.

The fix: health checks

yaml
services:
  app:
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5

Now Compose waits until pg_isready returns success (exit code 0) before starting the app. The health checkWhat is health check?An API endpoint that verifies your application and its dependencies are working, so monitoring tools can alert you when something fails. runs every 5 seconds, and after 5 consecutive successes, the service is considered healthy.

Common health check commands for popular services:

ServiceHealth check command
PostgreSQLpg_isready -U postgres
MySQLmysqladmin ping -h localhost
Redisredis-cli ping
MongoDBmongosh --eval "db.adminCommand('ping')"
05

Networks

Compose creates a default network for all services in the file. Services can reach each other by their service name, db is the hostname your app uses to connect to PostgreSQL.

# In your Python code
DATABASE_URL = "postgresql://postgres:postgres@db:5432/myapp"
#                                               ^^ service name as hostname

For most development setups, the default network is all you need. Custom networks become useful when you have multiple compose files that need to share services, or when you want to isolate groups of services.

06

Named volumes for data persistence

yaml
volumes:
  pgdata:

The volumes section at the top level declares named volumes. Named volumes survive docker compose down, your database data persists between restarts. Without a named volume, stopping and removing the containerWhat is container?A lightweight, portable package that bundles your application code with all its dependencies so it runs identically on any machine. deletes all data.

To wipe the volume and start fresh:

docker compose down -v    # -v removes named volumes
07

Quick reference

CommandWhat it does
docker compose upStart all services
docker compose up -dStart in background (detached)
docker compose up --buildRebuild images before starting
docker compose downStop and remove containers
docker compose down -vStop, remove containers, and delete volumes
docker compose logs appView logs for the app service
docker compose exec app bashOpen a shell in the running app container
docker compose psList running services
javascript
# compose.yaml, development setup for FastAPI + PostgreSQL
services:
  app:
    build: .
    ports:
      - "8000:8000"
    command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
    volumes:
      - ./src:/app/src
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/myapp
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:16-alpine
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: myapp
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5

volumes:
  pgdata: