Stage 4 · Provision
Docker Compose
Service Dependencies
Use depends_on, healthcheck conditions, wait-for scripts, and retry loops for startup ordering.
The Ordering Problem
When your API starts before the database is ready, it crashes. When the worker starts before the message queue is listening, it fails to connect. Startup ordering is one of the most common issues in multi-container applications. Compose provides several tools to handle this.
depends_on
The depends_on key controls startup order. By default, it waits for the dependency container to start (not to be ready). This is often insufficient — a database starting is not the same as a database accepting connections.
services:
api:
build: .
depends_on:
- db
- cache
db:
image: postgres:16-alpine
cache:
image: redis:7-alpineCompose starts db and cache first, then starts api. However, postgres might still be initializing when api starts. The depends_on only waits for the container to start, not for the service to be ready.
Healthcheck Conditions
The condition option in depends_on waits for a dependency's healthcheck to pass before starting the dependent service. This is the most reliable way to ensure services are ready before connecting.
services:
api:
build: .
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: app
POSTGRES_PASSWORD: secret
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 10
cache:
image: redis:7-alpine
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5The api service now waits until pg_isready returns 0 (PostgreSQL is accepting connections) and redis-cli ping returns PONG. This eliminates race conditions.
The service_started condition only waits for the container to start. The service_healthy condition waits for the healthcheck to pass. For databases and message queues, always use service_healthy.
Wait-For Scripts
For more complex readiness checks, you can use wait-for scripts that poll a port until it accepts connections. These are useful when healthchecks are not available or when you need custom readiness logic.
services:
api:
build: .
command: >
sh -c "wait-for-it db:5432 --timeout=30 &&
wait-for-it cache:6379 --timeout=30 &&
node server.js"
depends_on:
- db
- cache
db:
image: postgres:16-alpinewait-for-it is a shell script that waits for a TCP port to be available. It retries until the port responds or the timeout expires. Include it in your image or mount it from the host.
Retry Loops
Sometimes the simplest approach is a retry loop in your application code. Have your application retry connecting to dependencies with exponential backoff. This is more resilient than external waiting scripts.
services:
api:
build: .
environment:
DATABASE_URL: postgres://postgres:secret@db:5432/app
DB_RETRY_ATTEMPTS: 30
DB_RETRY_DELAY_MS: 1000
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 2s
timeout: 3s
retries: 20Combine service_healthy with application-level retries for maximum resilience. The healthcheck handles most cases. Application retries handle edge cases where the service briefly becomes unavailable.
Best Practices
- Always define healthchecks for databases, caches, and message queues
- Use condition: service_healthy in depends_on
- Design applications to retry connections on startup
- Do not depend on startup order alone — handle intermittent unavailability
- Use init containers or entrypoint scripts for schema migrations
- Log readiness status to help debug startup issues
PostgreSQL provides pg_isready as a lightweight health check. It returns 0 when the server is accepting connections and 1 otherwise. Use it in Docker healthchecks instead of running full SQL queries.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.