Service Topology in Compose
Service Topology in Compose
One file describes the whole stack; service names become hostnames on the default network:
services:
web:
build: ./web
ports: ["5173:5173"]
depends_on: [api]
api:
build: ./api
environment:
DATABASE_URL: postgres://app:pass@db:5432/app
depends_on:
db: { condition: service_healthy }
db:
image: postgres:16
environment: { POSTGRES_PASSWORD: pass, POSTGRES_USER: app }
volumes: ["pgdata:/var/lib/postgresql/data"]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
retries: 5
volumes:
pgdata:
Inside api, the host is literally db — no IP bookkeeping. condition: service_healthy turns the health check into an ordering guarantee.
Development versus Production Shapes
Development versus Production Shapes
Dev favors iteration: bind-mount source, run the dev server, let hot reload work.
web:
volumes: ["./web/src:/app/src"] # dev only
command: npm run dev
Prod favors immutability: multi-stage builds compile assets, and nginx serves them; the API runs the built output with NODE_ENV=production. Keep both shapes by overriding with a second compose file (-f compose.yml -f compose.prod.yml), so the base file stays the developer's default and CI never hand-edits YAML. Database migrations run as a one-off service (docker compose run api npm run migrate) before scaling the new API version.
Networking Pitfalls
Networking Pitfalls
The localhost trap. The browser runs on the host, not in the network — fetch("http://api:8080") from React fails because api resolves only inside Compose's DNS. Fix by proxying from the origin: nginx location /api/ { proxy_pass http://api:8080; } or Vite's server.proxy, so the browser talks to /api on its own host.
Ports are per-service. Only web needs a published port; publishing db exposes the database to your machine's whole network.
Secrets in YAML. .env files referenced via env_file keep credentials out of git; rotate anything that has ever been committed.
Interview: draw the three networks/ports involved, explain service DNS versus browser resolution, and describe startup ordering with health gates.
Quiz
1. Why does http://api:8080 work from the api's neighbor but not from the browser?
2. What does depends_on: { condition: service_healthy } guarantee?
3. Why publish only the web port?
Flashcards
Question
Service DNS
Click to reveal answer
Answer
Compose's embedded DNS resolves service names to container IPs on the project network.
Question
service_healthy
Click to reveal answer
Answer
depends_on condition that waits for a passing healthcheck before starting dependents.
Question
Proxy pattern
Click to reveal answer
Answer
Browser calls same-origin /api; nginx or Vite proxies to the api service name.
Question
Override file
Click to reveal answer
Answer
compose.prod.yml layered over compose.yml so dev and prod share one base.
Revision Notes
Key Takeaways
- 1. Service names are hostnames — inside the network only
- 2. Gate startup on healthchecks, not on container start
- 3. Publish ports for user-facing services only
Interview Tips
- • Draw request flow: browser → web/proxy → api → db and name each hop's DNS
- • Explain how migrations run relative to a new version going live
Cheat Sheet
web/api/db via service DNS; DATABASE_URL host=db; depends_on condition: service_healthy; browser reaches api via /api proxy; publish web port only.