A common Docker Compose bug: the app container starts, tries to connect to the database, and crashes because Postgres is still initializing. Plain depends_on only waits for the database container to start, not for the database to accept connections.
The fix is a healthcheck on the database plus condition: service_healthy on the app. Here is a complete setup.
App + Postgres
# app + PostgreSQL. `cp .env.example .env && docker compose up -d`
# The app waits until Postgres passes its healthcheck before starting.
name: app-postgres
services:
app:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER:?set in .env}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set in .env}
POSTGRES_DB: ${POSTGRES_DB:-app}
volumes:
- pgdata:/var/lib/postgresql/data # persists across `down`; `down -v` wipes it
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 3s
retries: 10
# No host port by default; uncomment to connect from local tools.
# ports: ["5432:5432"]
restart: unless-stopped
volumes:
pgdata:
With a .env next to it:
POSTGRES_USER=app
POSTGRES_PASSWORD=change-me
POSTGRES_DB=app
What each piece does
-
healthcheckwithpg_isready.pg_isreadyships in the Postgres image and returns success only when the server accepts connections. Compose runs it every 5 seconds, allows 3 seconds per attempt, and tries 10 times before marking the container unhealthy. -
$${POSTGRES_USER}(double dollar). A single$would make Compose substitute the value from your host shell when it parses the file.$$escapes it, so the variable is expanded inside the container, where it is set from theenvironmentblock. -
condition: service_healthy. The app is not created until the healthcheck passes. No sleep loops or wait-for-it scripts in your entrypoint. -
${POSTGRES_USER:?set in .env}. If the variable is missing,docker compose upstops with that error instead of booting a database with an empty user. -
${POSTGRES_DB:-app}. Falls back toappwhen unset. -
No published DB port. The app reaches the database over the Compose network as
db:5432. Nothing on your host or network can reach Postgres unless you uncomment theportsline. -
Named volume.
docker compose downkeeps the data;docker compose down -vwipes it. Worth knowing before you run the second one.
Same pattern for Redis
redis:
image: redis:7-alpine
command: ["redis-server", "--appendonly", "yes", "--maxmemory", "256mb", "--maxmemory-policy", "allkeys-lru"]
volumes:
- redisdata:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
restart: unless-stopped
redis-cli ping returns PONG once Redis is serving. The app then uses depends_on: { redis: { condition: service_healthy } } the same way. --appendonly yes turns on AOF persistence, and the memory flags cap Redis at 256 MB and evict least-recently-used keys when it is full, which suits a cache.
Checking it
docker compose up -d
docker compose ps # the db shows "(healthy)" before app starts
If the app never starts, docker inspect --format '{{json .State.Health}}' <db-container> shows the healthcheck output.
These files come from ShipKit's Docker & Dev Container Starter Pack, which also has multi-stage Dockerfiles for Node, Python, Go and static sites, a Compose Watch hot-reload setup, and dev container configs: https://8686702537545.gumroad.com/l/bafiks
Want to try one first? The free Node Dockerfile from the pack (multi-stage, non-root, healthcheck, hadolint-clean) is here: https://8686702537545.gumroad.com/l/csiqjo
Launch price: The full Docker & Dev Container pack, which includes the Compose files above, is $9 with code SHIPKIT9 until Thursday, Oct 15 at 11:59 PM CT (then $15): https://8686702537545.gumroad.com/l/bafiks/SHIPKIT9
Disclosure: I make ShipKit, which sells this pack. The snippets above are complete examples you can adapt to your own stack.
Top comments (0)