Blog
3 min read

Docker Compose Healthchecks and depends_on: Start Services in the Right Order

depends_on alone only waits for a container to start, not for it to be ready — which is why your app crashes with ECONNREFUSED while Postgres boots. How to write healthchecks for Postgres, MySQL, Redis and your own app, use condition: service_healthy, tune intervals, and debug unhealthy containers.

A classic Docker Compose bug: you run docker compose up, the database and app start together, and the app immediately crashes with ECONNREFUSED because Postgres is still initialising. You added depends_on: [db], so why?

Because plain depends_on only waits for the container to start, not for the service inside to be ready. The fix is a healthcheck plus condition: service_healthy. (ECONNREFUSED: Database Connection)

The pattern

services:
  db:
    image: postgres:18
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: app
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d app"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 20s

  app:
    build: .
    depends_on:
      db:
        condition: service_healthy
    environment:
      DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD}@db:5432/app

Now Compose starts db, waits until its healthcheck passes, and only then starts app.

The healthcheck options

Option Meaning
test The command to run. Exit code 0 = healthy
interval How often to run it
timeout How long a single check may take
retries Consecutive failures before "unhealthy"
start_period Grace period at startup where failures don't count
start_interval How often to check during the start period (newer Docker versions)

CMD runs the command directly; CMD-SHELL runs it through a shell (needed for ||, pipes or variables).

Healthchecks for common services

Postgres:

test: ["CMD-SHELL", "pg_isready -U postgres -d app"]

MySQL / MariaDB:

test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]

Redis:

test: ["CMD", "redis-cli", "ping"]

Your own web app — hit a health endpoint. Slim images often lack curl, so use whatever the image has:

# Node image without curl
test: ["CMD", "node", "-e", "fetch('http://localhost:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]

(Health Check Endpoints)

The three depends_on conditions

depends_on:
  db:
    condition: service_healthy                 # wait for healthcheck to pass
  migrate:
    condition: service_completed_successfully  # wait for a one-off job to exit 0
  cache:
    condition: service_started                 # the default: just started

service_completed_successfully is perfect for a migrations container that must finish before the app starts. (Prisma Migrations in Production)

Debugging an unhealthy container

docker compose ps                     # shows (healthy) / (unhealthy) / (starting)
docker inspect --format '{{json .State.Health}}' myproject-db-1

The inspect output includes the last few check results with their output, which usually tells you exactly what failed: wrong username, missing binary in the image, wrong port.

Healthchecks don't restart things (in plain Docker)

In plain Docker and Compose, an unhealthy container is just marked unhealthy — it isn't restarted automatically. restart: unless-stopped only reacts when the process exits. Orchestrators like Docker Swarm and Kubernetes act on health status; with Compose, your app should still handle a database blip by retrying connections. (Exponential Backoff, Docker Container Keeps Restarting)

Don't make the healthcheck too clever

A healthcheck that queries the database, calls three APIs and checks disk space will fail for reasons that have nothing to do with whether this container works — and with load balancers, that takes your whole app offline. Check that this service can answer.

The summary

  • depends_on waits for start, not readiness. Add a healthcheck and condition: service_healthy.
  • Use pg_isready, mysqladmin ping, redis-cli ping, or your app's /health.
  • Use service_completed_successfully for migration jobs; debug with docker inspect.

EasySpawn runs your app and its database on a managed server with startup order and health monitoring handled, and Claude Code can debug a container that won't go healthy. See how it works or join the waitlist.

Related: Docker Compose for Local Development · Docker vs Docker Compose · Docker Compose Networking · PID 1 in Containers

Keep reading