A checkout-worker container in your docker-compose.yml can't reach postgres-db even though `docker compose ps` shows both as running and healthy — walk through how you would systematically isolate whether this is a DNS, network attachment, or application-level failure.
Quick Answer
Start from the network layer outward: confirm both containers are actually attached to the same Compose network, then verify DNS resolution of the service name from inside the calling container, then test raw TCP connectivity to the port, and only then look at application-level auth or config errors. Most 'can't connect' incidents in Compose turn out to be a service accidentally left off a custom network, or a port mismatch between what the app is listening on inside the container versus what's configured downstream.
Detailed Answer
Think of this like troubleshooting why a phone call between two departments in an office isn't connecting. You don't start by suspecting the recipient's phone is broken — you first check whether both extensions are even on the same phone system, then whether dialing the extension number rings anywhere at all, then whether the call connects but nobody picks up, and only last whether the person who answers is refusing the specific request being made. Networking debugging should follow the same outside-in narrowing, because jumping straight to 'the app must be misconfigured' skips several cheaper, faster checks.
Compose intentionally exposes each of these layers separately so you can test them independently: docker network inspect shows you attachment (is the container even plugged into the network), docker exec ... nslookup shows you name resolution, docker exec ... nc -zv shows you raw TCP reachability, and only after those three pass does an actual failed connection point at something inside the application — wrong credentials, wrong database name, or a startup race condition. Skipping straight to reading application logs is the single most common reason engineers waste 30+ minutes on what turns out to be a one-line networks: misconfiguration.
Step by step: first, confirm attachment — run docker inspect checkout-worker and check .NetworkSettings.Networks, and do the same for postgres-db; if they're not on a common network key, that's your root cause immediately, usually because someone added a custom networks: block to one service and forgot the default network no longer auto-attaches once you define an explicit one. Second, exec into checkout-worker and run getent hosts postgres-db — if this fails to resolve, either the network attachment is wrong (step one already caught it) or the service was renamed and stale DNS caching in a long-lived connection pool is still holding an old IP. Third, test the actual port with nc -zv postgres-db 5432 from inside checkout-worker — if DNS resolves but the port doesn't respond, the app inside postgres-db likely isn't listening yet (a health check gap) or is bound to localhost inside its own container instead of 0.0.0.0. Only once TCP connects and you still see errors do you move to application logs for auth failures, wrong database names, or connection pool exhaustion.
At scale, this exact failure pattern is the most common cause of 'flaky' integration tests in CI — the network layer is fine, but the healthcheck is missing or too permissive, so checkout-worker starts before postgres-db's TCP listener is actually open, producing intermittent, hard-to-reproduce failures depending on scheduling luck. Teams that add condition: service_healthy on depends_on convert this from an intermittent CI flake into a deterministic wait, which is why it's worth checking as step zero before diving into DNS at all.
The gotcha: docker compose ps reporting a container as 'healthy' only reflects what its own healthcheck test command checked — it says nothing about whether a *different* service can actually reach it, since healthchecks run from inside the container's own namespace. Engineers regularly get burned assuming 'healthy' means 'reachable from anywhere,' when in reality a container can pass its own loopback-based healthcheck while still being unreachable from a sibling container due to a firewall rule inside the image or a service incorrectly binding to 127.0.0.1 instead of all interfaces.