LearnDevOpsHands-On Labs
Troubleshooting Matrix/Connection refused between Compose services on startup
DockerHigh Severity

Connection refused between Compose services on startup

Fix Compose "connection refused" at startup: add a healthcheck and depends_on with condition: service_healthy. Also covers migration gates and app retries.

Observed Error Signature / Log Pattern
dial tcp 172.18.0.3:5432: connect: connection refused

Incident Overview

Fix Compose "connection refused" at startup: add a healthcheck and depends_on with condition: service_healthy. Also covers migration gates and app retries.

medium fix·6 min read

By Vishvesh Patel·DevOps Engineer·Updated 21 September 2026

BASHDiagnostic / remediation command
dial tcp 172.18.0.3:5432: connect: connection refused

psycopg2.OperationalError: could not connect to server: Connection refused
        Is the server running on host "postgres" (172.18.0.3) and accepting TCP connections on port 5432?

Error: getaddrinfo ENOTFOUND postgres

Do this first3 steps

Run these in order. Each one tells you what its output means before you change anything.

• Confirm the dependency is up but not yet ready

A dependency showing "Up" without a health status means Compose considers depends_on satisfied while the process inside may still be initialising. That gap is the whole problem.

• Add a healthcheck to the dependency and gate on it

depends_on with condition service_healthy waits for the healthcheck to pass rather than for the container to exist. Without a healthcheck defined, that condition cannot be used.

• Verify the wait actually happens

With a correct healthcheck the dependent service should not start until the dependency reports healthy. If it still starts immediately, the condition is missing or the healthcheck is trivially passing.

depends_on controls start order, not readiness. Compose starts the dependency's container first and then immediately starts yours. A Postgres container takes several seconds to initialise, and during that window it is running and refusing connections.

So the classic Compose file is wrong in a way that only fails sometimes:

On a warm machine the API might win the race. On a cold start, or in CI, it does not.

BASHDiagnostic / remediation command
docker compose ps

Confirm the shape of the failure

The dependency is up, yours exited. That is the race.

Distinguish it from a networking problem by the error text:

connection refused is good news in a sense: DNS worked, so the services are on the same network and the name is right.

BASHDiagnostic / remediation command
docker compose ps

Fix 1: Healthchecks and service_healthy

Note the depends_on syntax changes from a list to a map when you add conditions. Mixing the two forms is a common mistake and Compose will tell you.

Three conditions are available:

start_period is the one people leave out. During it, failing checks do not count towards retries, which stops a slow-starting database being marked unhealthy before it has had a chance.

BASHDiagnostic / remediation command
services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: example
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 10
      start_period: 10s

  api:
    build: .
    depends_on:
      postgres:
        condition: service_healthy

Getting the healthcheck right

pg_isready returns success as soon as the server accepts connections, which can be before the init scripts in /docker-entrypoint-initdb.d have finished. If your app expects a schema, check for it:

Common ones for other services:

CMD-SHELL runs the string through a shell, so || and pipes work. CMD is exec form with no shell. The HTTP one needs curl or wget inside the image, which distroless and slim images often lack.

BASHDiagnostic / remediation command
healthcheck:
      test: ["CMD-SHELL", "psql -U postgres -d appdb -c 'SELECT 1' || exit 1"]

Fix 2: Run migrations as a gate

Compose waits for migrate to exit 0. A non-zero exit stops the API starting at all, which is what you want: an app running against an un-migrated schema fails in more confusing ways later.

BASHDiagnostic / remediation command
services:
  migrate:
    build: .
    command: ["./migrate", "up"]
    depends_on:
      postgres:
        condition: service_healthy

  api:
    build: .
    depends_on:
      migrate:
        condition: service_completed_successfully
      postgres:
        condition: service_healthy

The real fix: retry in the application

Compose conditions only cover startup. They do nothing when the database restarts at three in the morning, or when a connection drops mid-request. An application that retries its own connection handles both, and makes the Compose configuration a convenience rather than a requirement.

Most frameworks and connection pools do this already. Check before writing it yourself.

restart: unless-stopped is the crude version: let the container die and be restarted until the dependency is up. It works, and it fills the logs with crashes and makes a genuine startup bug indistinguishable from a race.

BASHDiagnostic / remediation command
import time, psycopg2

def connect(retries=30, delay=2):
    for attempt in range(retries):
        try:
            return psycopg2.connect(host="postgres", dbname="appdb", user="postgres")
        except psycopg2.OperationalError as e:
            if attempt == retries - 1:
                raise
            print(f"db not ready ({attempt+1}/{retries}): {e}", flush=True)
            time.sleep(delay)

ENOTFOUND is a different problem

If the name does not resolve at all, the two services are not on the same network, or the name is wrong.

The hostname is the service key in the Compose file, not container_name. A service named postgres with container_name: db is still reachable as postgres.

Also note that Compose puts every service on a default network per project. Explicit networks: on some services and not others splits them, which produces exactly this.

BASHDiagnostic / remediation command
docker compose exec api getent hosts postgres
docker network inspect $(docker compose ps -q postgres | head -1) | grep -A5 Networks

A checklist

Frequently Asked Questions

Why does depends_on not wait for my database to be ready?

Because by default it only waits for the container to be started, not for the process inside to be accepting connections. A Postgres container reports as running within a second and then spends several more initialising, and during that window connections are refused. depends_on in its plain list form has always meant ordering rather than readiness. To wait for readiness you need the map form with condition: service_healthy, which requires the dependency to define a healthcheck.

What is the difference between service_started, service_healthy and service_completed_successfully?

service_started is the original behaviour: the container has been started, nothing more. service_healthy waits for the dependency's healthcheck to report healthy, which is what you want for databases and other long-running services. service_completed_successfully waits for the container to exit with status zero, which is the right condition for one-shot work such as a migration or a seed job. They can be combined, so an API can wait on both a healthy database and a completed migration.

Why did Compose wait correctly and my app still fail?

Usually the healthcheck passes earlier than real readiness. pg_isready succeeds as soon as Postgres accepts connections, which can be before the scripts in /docker-entrypoint-initdb.d have created your schema, so the app connects and then fails on a missing table. Make the check assert what your application actually needs, for example a psql -c 'SELECT 1' -d appdb against the specific database. The other cause is start_period being absent, so a slow start exhausts retries and the dependency is marked unhealthy.

Should I use a wait script like wait-for-it instead?

They work and they solve a narrower problem than people think. A wait script only covers process startup; it does nothing when the database restarts later or a connection drops mid-request. Compose healthchecks are the cleaner version of the same idea, and they need no extra binary in the image. The durable answer is retry logic in the application or its connection pool, which handles both the startup race and every subsequent blip, and most frameworks already provide it.

Why does my service name not resolve at all?

ENOTFOUND or no such host means DNS failed rather than the connection being refused, so this is a networking problem rather than a timing one. Check that you are using the service key from the Compose file as the hostname, not container_name, which does not create a DNS alias. Then confirm both services share a network: Compose puts everything on one default network per project, but adding an explicit networks: list to some services and not others splits them, and the two halves cannot resolve each other.

Reference and practice

Learn the underlying concept

Other Docker errors

Free, comprehensive DevOps tutorials and learning roadmaps. Master Docker, Kubernetes, CI/CD, and more.

Get notified about new tutorials and features.

• Roadmaps

• Interview Prep

• Certifications

• Company

© 2026 DevOpsLesson. All rights reserved.

Need to practice this scenario?

Simulate this error condition and practice recovery commands safely in our in-browser terminal.

Open Docker Sandbox