Docker & DevOps

Docker Compose depends_on and healthchecks: get startup order right

Use depends_on with condition: service_healthy so your app waits until the database accepts connections, and learn how to tune healthcheck timing without slowing every start.

IntermediateLinux server7 min read

Docker Compose depends_on on its own only controls the order in which containers are created and started — it does not wait for the database inside the container to accept connections. To stop your app from starting before Postgres or MySQL is ready, pair depends_on with condition: service_healthy and give the database a healthcheck that reports when it is actually serving queries.

This guide walks through a working Compose file, the long depends_on syntax, one-shot migration jobs, and the timing settings that decide whether a start takes five seconds or two minutes.

Before you start

  • A Linux VPS running Docker Engine with the Compose v2 plugin. Confirm with docker compose version. If that command is missing, follow Install Docker and Docker Compose on a Linux VPS.
  • A Compose file with at least two services — typically an application and a database.
  • The condition: form of depends_on requires Compose v2 (the docker compose plugin). The older Python docker-compose 1.x only understood it in file format 2.x, so upgrade instead of fighting it.
  • Remove any top-level version: key from the file. Compose v2 ignores it and prints a warning.

Tip: Test in a copy of the file or on a git branch so you can compare startup behaviour before and after the change.

Why depends_on alone is not enough

The short syntax looks harmless:

YAML
services:
  app:
    depends_on:
      - db

Compose starts db, sees the container is running, and immediately starts app. Postgres needs a few seconds to initialise its data directory before it listens on port 5432, so the app exits with connection refused, could not connect to server, or a driver-specific ECONNREFUSED. With a restart policy the container loops until the database catches up, which works but produces noisy logs and slow starts.

The long syntax adds a condition. Compose supports three:

  • service_started — the container is running. Same as the short syntax.
  • service_healthy — the container's healthcheck reports healthy.
  • service_completed_successfully — the container ran and exited with code 0.

Step 1: Give the database a healthcheck

A healthcheck is a command Compose runs inside the container on a schedule. Exit code 0 means healthy.

YAML
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: deploy
      POSTGRES_PASSWORD: example
      POSTGRES_DB: app
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U deploy -d app"]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 30s
    volumes:
      - db_data:/var/lib/postgresql/data

volumes:
  db_data:

pg_isready returns 0 only when the server accepts connections, which is exactly the signal the app needs. For MySQL or MariaDB, use a ping instead:

YAML
    healthcheck:
      test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u root -p$$MYSQL_ROOT_PASSWORD --silent"]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 30s

The double $$ escapes the dollar sign so Compose passes $MYSQL_ROOT_PASSWORD through to the shell inside the container rather than substituting it on the host.

Note: start_period is a grace window. Failures during it do not count towards retries, so a slow first-time initialisation does not mark the container unhealthy.

Step 2: Point depends_on at the healthy condition

Now the app service waits for that health status:

YAML
  app:
    build: .
    depends_on:
      db:
        condition: service_healthy
    environment:
      DATABASE_URL: postgres://deploy:example@db:5432/app
    ports:
      - "8080:8080"

Compose resolves the hostname db through the project network, so the connection string uses the service name, not localhost or 127.0.0.1. If your app connects to 127.0.0.1 inside its own container it will never reach the database, no matter how the startup order is configured.

You can list several dependencies, each with its own condition:

YAML
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_healthy

Step 3: Start the stack and watch the order

Bash
docker compose up -d
docker compose ps
docker compose logs -f app

docker compose ps shows a STATUS column with Up 12 seconds (healthy) once the check passes. The app container stays in Created until then, so the log stream for app stays empty for the first few seconds — that is expected, not a hang.

To make the command itself block until everything is healthy, add --wait:

Bash
docker compose up -d --wait

This is useful in deploy scripts: the command returns non-zero if a service never becomes healthy, so a broken release fails the pipeline instead of going live.

Step 4: Wait for one-shot jobs with service_completed_successfully

Database migrations are a common third service. They must run after the database is healthy and finish before the app starts.

YAML
  migrate:
    build: .
    command: ["./manage.py", "migrate", "--noinput"]
    depends_on:
      db:
        condition: service_healthy

  app:
    build: .
    depends_on:
      migrate:
        condition: service_completed_successfully
      db:
        condition: service_healthy

Warning: Do not give the migration service a restart: always policy. A one-shot container that exits 0 would be restarted forever, and service_completed_successfully would never be satisfied in the way you expect. Leave the restart policy off for job containers.

Step 5: Tune the timing

The defaults are conservative. Adjust them so a healthy start is fast and a genuinely broken start still fails:

  • interval — how often the check runs. 5s is a good default; 1s adds load for little gain.
  • timeout — how long a single check may take before it counts as a failure.
  • retries — consecutive failures before the container is marked unhealthy. 10 with a 5s interval gives roughly 50 seconds of tolerance after start_period.
  • start_period — set this to your worst realistic initialisation time, especially on first boot when the data directory is empty.

Note: A failing healthcheck marks the container unhealthy; it does not restart it. If you want automatic replacement, that is a separate mechanism — see Docker restart policies: keep containers running after a reboot.

Also remember that depends_on only applies at start time. If the database restarts later, Compose does not restart the app for you, so keep reconnect logic in the application or a restart policy on the app container.

Check it works

  1. Bring the stack down and up again: docker compose down && docker compose up -d --wait.
  2. Confirm the health status directly: docker inspect --format '{{.State.Health.Status}}' $(docker compose ps -q db) — expect healthy.
  3. Confirm the app started after the database by checking timestamps: docker compose logs --timestamps db app | head -40.
  4. Validate the file before deploying: docker compose config prints the resolved configuration and fails on syntax errors.

Troubleshooting

  • App still exits with connection refused — the healthcheck passes too early. pg_isready confirms the server accepts connections but not that your database and user exist yet. Check the credentials in DATABASE_URL and lengthen start_period.
  • depends_on condition is ignored, or Compose errors with an invalid type — you are running the legacy docker-compose 1.x binary or a file format 3.x file with an old CLI. Switch to docker compose (v2) and drop the version: key.
  • docker compose up waits forever and never starts the app — the dependency never becomes healthy. Run docker compose logs db and docker compose ps to see whether the container is restarting, and test the healthcheck command manually with Open a shell inside a running Docker container.
  • Healthcheck reports command not found — the image does not include curl or wget. Use a tool that ships with the image, or install it in your own Dockerfile.
  • Everything is healthy but the app cannot reach the database — the app is connecting to 127.0.0.1 instead of the service name, or the two services are on different networks. See Troubleshooting container networking issues: bridge vs host mode in Docker.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.