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:
# 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.
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
services:
# Built from your Dockerfile
app:
build: .
# Pulled from Docker Hub
db:
image: postgres:16-alpineFor 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
ports:
- "8000:8000" # host:containerThe 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:
# 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:
env_file:
- .envMake sure .env is in your .gitignore and .dockerignore.
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.
volumes:
- ./src:/app/src # your code → container
- /app/__pycache__ # anonymous volume: keep container's cacheThe 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:
services:
app:
build: .
command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
volumes:
- ./src:/app/srcThe command override replaces the CMD from your Dockerfile. In production, you remove this override and use the Dockerfile's CMD.
The depends_on trap
This is the most common bug in AI-generated compose files.
# What AI generates
services:
app:
depends_on:
- db
db:
image: postgres:16-alpinedepends_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.
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
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: 5Now 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:
| Service | Health check command |
|---|---|
| PostgreSQL | pg_isready -U postgres |
| MySQL | mysqladmin ping -h localhost |
| Redis | redis-cli ping |
| MongoDB | mongosh --eval "db.adminCommand('ping')" |
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 hostnameFor 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.
Named volumes for data persistence
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 volumesQuick reference
| Command | What it does |
|---|---|
docker compose up | Start all services |
docker compose up -d | Start in background (detached) |
docker compose up --build | Rebuild images before starting |
docker compose down | Stop and remove containers |
docker compose down -v | Stop, remove containers, and delete volumes |
docker compose logs app | View logs for the app service |
docker compose exec app bash | Open a shell in the running app container |
docker compose ps | List running services |
# 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: