A container shown as "Up" is not always ready to serve requests: the process may have started while the database is still loading. That is the difference between running and being healthy. In this guide you will see how to define a healthcheck in Docker so Compose knows when a service really works, which restart policy to choose so a downed container recovers on its own, and how to chain services with depends_on so your application does not start before its database. The same guide is available in Spanish.

Key takeaways

  • A healthcheck is a command Docker runs inside the container at a set interval; based on its exit code (0 healthy, 1 unhealthy) the state moves from starting to healthy or unhealthy.
  • The default values, inherited from the Dockerfile HEALTHCHECK instruction, are interval: 30s, timeout: 30s, retries: 3 and start_period: 0s.
  • There are four restart policies: no (the default), always, on-failure and unless-stopped; the key difference is what happens after the Docker daemon restarts.
  • depends_on supports three conditions: service_started, service_healthy and service_completed_successfully. Only service_healthy waits for the healthcheck to pass.
  • on-failure accepts a retry limit (on-failure:5), handy to avoid an infinite restart loop.
  • The start_interval attribute (more frequent checks during startup) requires Docker Compose v2.20.2 and Docker Engine 25.0 or later.

What is a healthcheck in Docker and why does it matter?

When you run docker compose ps, a container can show as Up as soon as its main process starts. But "started" does not mean "ready": a database takes a few seconds to accept connections, and a web server may still be spinning up the interpreter before it answers. Docker’s documentation puts it bluntly: "On startup, Compose does not wait until a container is ‘ready’, only until it’s running." That gap causes half of the startup errors in a multi-service docker-compose.yml.

A healthcheck closes that gap. It is a command Docker runs periodically inside the container; if it exits with code 0 the container is considered healthy, and if it exits with 1, unhealthy. While the initial start_period holds, failures do not count; after that, once enough consecutive failures pile up (retries), Docker marks the container as unhealthy. That state is visible in docker compose ps and, above all, other services can wait on it. With no engine installed there is nothing to check; if you are starting from scratch, review how to install Docker on Ubuntu 24.04 first.

How do you define a healthcheck in Compose?

The healthcheck is declared inside the service, with six possible fields. The only required one is test, the command that decides health:

services:
  web:
    image: nginx:1.27.2
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/"]
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 20s
      start_interval: 5s

The test field takes three forms. ["CMD", "command", "arg"] runs the binary directly, without a shell. ["CMD-SHELL", "command with pipes"] runs it through the container’s shell, needed if you use pipes or variables. And ["NONE"] disables any healthcheck inherited from the image. A bare string is equivalent to CMD-SHELL.

The other fields tune the rhythm. interval is the time between checks; timeout, how long Docker waits before calling a check failed; retries, how many consecutive failures are needed to mark it unhealthy; and start_period, a grace window at startup during which failures do not penalise. As the documentation notes, the healthcheck "works in the same way, and has the same default values, as the HEALTHCHECK Dockerfile instruction": interval and timeout are 30 seconds, retries is 3 and start_period is 0. The start_interval field lets you check every few seconds during startup, to detect sooner that the service is already healthy, but it requires Docker Engine 25.0 or later.

Restart policies: no, always, on-failure and unless-stopped

The healthcheck reports; the restart policy acts. It is declared with the restart key and determines what Docker does when the container stops. There are four values:

  • no: the default. The container is not restarted under any circumstances.
  • always: it restarts whenever it stops and also when the Docker daemon starts, until you remove it.
  • on-failure: it restarts only if the container exits with a non-zero code (an error). It accepts an optional retry cap, on-failure:5.
  • unless-stopped: like always, but if you stop the container manually, it is not restarted after the daemon restarts.

The difference between always and unless-stopped is subtle but important: after a manual docker stop followed by a server reboot, always brings the container back up while unless-stopped respects your decision to leave it stopped. For homelab services you want to survive a host reboot, unless-stopped is usually the safer choice; it is the one we use in every example of this series. Bear in mind one nuance: the documentation warns that "restart policies only apply to containers", so under Docker Swarm the restart key is ignored and the service’s own restart config is used.

depends_on with the service_healthy condition

This is where the healthcheck stops being informational and becomes actionable. By default, depends_on only guarantees startup order, not that the dependency is ready. The long form of depends_on accepts three conditions:

  1. service_started: waits only until the container is running (the classic behaviour).
  2. service_healthy: waits until the dependency is healthy according to its healthcheck.
  3. service_completed_successfully: waits until the dependency exits with code 0, useful for init or migration containers.

With service_healthy, Compose does not start your application until the database has passed its healthcheck. It is the piece that eliminates "connection refused" errors when bringing the stack up. Before chaining services it helps to be clear on the configuration they receive: review environment variables and secrets in Docker to pass passwords safely, and if you want to enable or disable whole services per environment, look at Docker Compose profiles.

A complete example with a database and an app

This docker-compose.yml brings it all together: a PostgreSQL database with its healthcheck, an application that waits for it to be healthy, and a restart policy on both services. It is direct copy and paste:

services:
  db:
    image: postgres:16.4
    restart: unless-stopped
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: myapp
      POSTGRES_PASSWORD: change_this_key
    volumes:
      - db_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U myapp -d myapp"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  app:
    image: nginx:1.27.2
    restart: unless-stopped
    depends_on:
      db:
        condition: service_healthy
    ports:
      - "8080:80"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost/"]
      interval: 15s
      timeout: 5s
      retries: 3
      start_period: 20s

volumes:
  db_data:

Bring the stack up and watch the app wait for the database before starting:

docker compose up -d
docker compose ps
docker inspect --format '{{.State.Health.Status}}' $(docker compose ps -q db)

The STATUS column of docker compose ps will show (healthy) next to the db container as soon as pg_isready answers, and only then will you see app start. The last command prints the raw health state (starting, healthy or unhealthy) for debugging. If you need to tune the database itself, the guide on how to install PostgreSQL with Docker goes into detail on the pg_isready healthcheck and on backups.

A common warning: never pin the image to :latest in production. It is not reproducible and a silent update can change the healthcheck binary (for example, if curl stops shipping in the image). Pin a specific version such as postgres:16.4 and keep :latest only as an example of what not to do.

Frequently asked questions

What is the difference between depends_on and a healthcheck?

depends_on controls the order in which Compose starts services; the healthcheck controls whether a service is healthy. On their own, depends_on without a condition only waits for the container to be running. The combination of depends_on with condition: service_healthy is what actually waits for the dependency to pass its healthcheck.

What is the difference between always and unless-stopped?

Both restart the container when it fails. The difference shows up after a Docker daemon restart: always brings the container back even if you had stopped it manually, while unless-stopped respects that manual stop and leaves it down. For homelab services, unless-stopped is usually the most predictable.

Why does my container stay in starting and never turn healthy?

It is almost always the test command. Check that the binary exists inside the image (many minimal images ship neither curl nor wget), that the port and path are correct, and that start_period gives the service enough time to initialise. Run the test command by hand with docker compose exec to see the real error.

Conclusion

A well-built service watches and heals itself. The healthcheck tells Docker when your application is truly ready, the restart policy brings it back up if it falls, and depends_on with service_healthy orders startup so nothing runs too early. With those three mechanisms, a container stack survives server reboots and slow starts without manual intervention. The natural next step is organising optional services per environment: continue with Docker Compose profiles.

Sources: [1] Docker Docs: Services top-level element (healthcheck)[1], [2] Docker Docs: Start containers automatically (restart policies)[2], [3] Docker Docs: Control startup and shutdown order (depends_on)[3], [4] docker/compose Releases (start_interval, v2.20.2)[4], [5] Docker Hub: PostgreSQL official image (pg_isready)[5].

Sources

  1. Docker Docs: Services top-level element (healthcheck)
  2. Docker Docs: Start containers automatically (restart policies)
  3. Docker Docs: Control startup and shutdown order (depends_on)
  4. docker/compose Releases (start_interval, v2.20.2)
  5. Docker Hub: PostgreSQL official image (pg_isready)

Route: Self-hosting with Docker: from zero to production