Docker Compose
Service definitions, depends_on, profiles, and compose file structure. You'll hit this when your local dev stack has five services and you need them to start in the right order.
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
app:
build: .
depends_on:
- dbservices:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
app:
build: .
depends_on:
- dbservices:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
app:
build: .
depends_on:
db:
condition: service_healthyservices:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
app:
build: .
depends_on:
db:
condition: service_healthyPlain depends_on only waits for the container to start, not for the service inside to be ready. Your app will crash with connection errors because PostgreSQL takes a few seconds to initialize after the container starts.
Using condition: service_healthy with a proper healthcheck ensures the app container only starts when the database is actually ready to accept connections. The pg_isready check verifies PostgreSQL is listening, not just that the container is running.
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
# No volume - data lost on restartservices:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
# No volume - data lost on restartservices:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:Without a volume, all database data lives in the container's writable layer. Running docker compose down or recreating the container deletes everything. Development data, test records, and migration state all vanish.
Named volumes persist data across container restarts and docker compose down. The pgdata volume is managed by Docker and survives container lifecycle events. Defining it in the top-level volumes section makes it explicit and shareable.
services:
app:
build: .
ports:
- "3000:3000"
# Always starts, even when not needed
mailhog:
image: mailhog/mailhog
ports:
- "8025:8025"
# Always starts, even when not needed
adminer:
image: adminer
ports:
- "8080:8080"services:
app:
build: .
ports:
- "3000:3000"
# Always starts, even when not needed
mailhog:
image: mailhog/mailhog
ports:
- "8025:8025"
# Always starts, even when not needed
adminer:
image: adminer
ports:
- "8080:8080"services:
app:
build: .
ports:
- "3000:3000"
mailhog:
image: mailhog/mailhog
ports:
- "8025:8025"
profiles:
- debug
adminer:
image: adminer
ports:
- "8080:8080"
profiles:
- debugservices:
app:
build: .
ports:
- "3000:3000"
mailhog:
image: mailhog/mailhog
ports:
- "8025:8025"
profiles:
- debug
adminer:
image: adminer
ports:
- "8080:8080"
profiles:
- debugWithout profiles, every service starts on docker compose up, even debugging tools you rarely need. This wastes resources, clutters logs, and slows down your development workflow. Every developer pays the cost whether they need the tools or not.
Profiles let you define optional services that only start when explicitly requested with docker compose --profile debug up. This keeps the default docker compose up fast and lightweight while debug tools remain available on demand.
services:
frontend:
build: ./frontend
ports:
- "3000:3000"
backend:
build: ./backend
db:
image: postgres:16
# All services on default network
# Frontend can reach DB directlyservices:
frontend:
build: ./frontend
ports:
- "3000:3000"
backend:
build: ./backend
db:
image: postgres:16
# All services on default network
# Frontend can reach DB directlyservices:
frontend:
build: ./frontend
ports:
- "3000:3000"
networks:
- frontend
backend:
build: ./backend
networks:
- frontend
- backend
db:
image: postgres:16
networks:
- backend
networks:
frontend:
backend:services:
frontend:
build: ./frontend
ports:
- "3000:3000"
networks:
- frontend
backend:
build: ./backend
networks:
- frontend
- backend
db:
image: postgres:16
networks:
- backend
networks:
frontend:
backend:The default Compose network puts all services on the same network. Any service can reach any other service directly. The frontend can bypass the backend and query the database, which is a security risk and an architectural violation.
Explicit networks isolate services by tier. The frontend can only reach the backend, and the backend can reach both frontend and database. The frontend cannot access the database directly. This follows the principle of least privilege and mirrors production topology.