DEV Community

Anthany Wingo
Anthany Wingo

Posted on

Stop your app from starting before Postgres is ready: Compose healthchecks done right

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:
Enter fullscreen mode Exit fullscreen mode

With a .env next to it:

POSTGRES_USER=app
POSTGRES_PASSWORD=change-me
POSTGRES_DB=app
Enter fullscreen mode Exit fullscreen mode

What each piece does

  • healthcheck with pg_isready. pg_isready ships 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 the environment block.
  • 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 up stops with that error instead of booting a database with an empty user.
  • ${POSTGRES_DB:-app}. Falls back to app when 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 the ports line.
  • Named volume. docker compose down keeps the data; docker compose down -v wipes 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)