Defining a full multi-container dev stack (app + Postgres + Redis + Kafka) in one file, and the important gap between depends_on's startup ORDER and a dependency actually being READY.
Published September 23, 2026
services:
app:
build: .
ports:
- "8080:8080"
environment:
- SPRING_PROFILES_ACTIVE=local
- DB_HOST=postgres
depends_on:
- postgres
- redis
postgres:
image: postgres:16
environment:
- POSTGRES_PASSWORD=devpassword
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
kafka:
image: confluentinc/cp-kafka:latest
depends_on:
- zookeeper
volumes:
pgdata:
One docker-compose.yml file describes an entire local development environment — the application plus every infrastructure dependency it needs (a database, a cache, a message broker) — as a single unit, started together with one command (docker-compose up). This is what replaces the historically painful "install Postgres, install Redis, install Kafka, configure each one locally, hope versions match production" onboarding process with a single, versioned, reproducible file every developer on the team runs identically.
As covered in Networking & Storage, compose automatically creates a shared network for all services in the file — app reaching postgres simply uses the hostname postgres (the service name), with compose's built-in DNS resolving it, no manual network configuration required at all.
app:
depends_on:
- postgres # guarantees postgres CONTAINER STARTS before app container starts
# does NOT guarantee postgres is actually ACCEPTING CONNECTIONS yet
This is the single most important, most commonly misunderstood point about depends_on: it controls STARTUP ORDER (the postgres container process begins before the app container process begins) — it does not wait for postgres to actually be ready to accept connections. A database container can take several seconds after its process starts before it's genuinely ready to serve queries (initializing its data directory, running startup migrations) — an app container that assumes depends_on means "ready" will frequently fail its first connection attempt in local development, a confusing, easy-to-misdiagnose failure for anyone new to compose.
The correct fix is the application's OWN retry/backoff logic on startup (the same resilience thinking as Circuit Breaker Pattern and Timeout Strategy, applied to a dependency at STARTUP rather than at request time) — or, in more recent compose versions, an explicit condition: service_healthy on depends_on, tied to the dependency's own HEALTHCHECK (Health Checks & Production Best Practices), which DOES wait for genuine readiness rather than just process start.
app:
environment:
- SPRING_PROFILES_ACTIVE=local # inline, hardcoded here
env_file:
- .env.local # OR loaded from a separate file
Environment variables can be set inline per service, or loaded from a .env file — the file-based approach is generally preferred for anything that varies per developer or contains local-only values, since it keeps the compose file itself identical across everyone's machines while letting each developer's .env.local (typically gitignored) hold their own local overrides.
Q: Why not just use condition: service_healthy everywhere instead of relying on app-level retry logic?
A: service_healthy is a genuinely good fix WHEN the dependency has a well-defined health check already, but the app's own retry/backoff logic is still valuable as defense-in-depth — a production deployment (Kubernetes, not compose) doesn't have depends_on semantics at all, so an application that only works because compose sequenced things correctly will break in a real orchestrated environment; building retry logic into the app itself is the more portable, universally-correct fix.
Q: Does a local docker-compose stack accurately represent how services actually communicate in production? A: Only loosely — compose's shared bridge network and by-name DNS resolution is conceptually similar to Kubernetes Services (Services & Ingress), but the actual mechanisms differ; compose is valuable for LOCAL functional testing (does my code correctly integrate with a real Postgres/Redis), not as a stand-in for production networking or scaling behavior.
Q: Should local-only values like devpassword ever resemble real credentials?
A: No — local dev credentials should be obviously fake/throwaway values, specifically so no one is tempted to reuse them anywhere real, and so an accidentally-committed compose file with a hardcoded local password poses zero actual security risk.
Q: How does a compose-defined volume (like pgdata above) relate to what's covered in Networking & Storage?
A: It's the exact same named-volume mechanism — compose's volumes: top-level key just declares it in the same file as the services using it, for convenience; the underlying Docker volume behavior (persists independently of any one container's lifecycle) is identical to running docker volume create manually.