A container gets its own network namespace, with its own interfaces, routing table, and loopback. Inside the Waybill container, localhost is Waybill. Ledger listening on 5432 in its container is invisible there, which is why a connection to localhost:5432 is refused even though "Ledger is definitely listening".
Compose, Docker networks, and Kubernetes solve discovery the same way: an embedded DNS server resolves service names to the current addresses of their containers. Connect to ledger:5432, not to an IP address. IPs change whenever a container is recreated, and a hardcoded address fails at the next deploy.
Keep peers private:
- Services on the same network reach each other on container ports. Publishing a port (
ports:) maps it onto the host, which is only needed for callers outside Docker. Publishing a database to makelocalhostwork exposes it to anything that can reach the host. internal: truenetworks have no route out. That is good for backends and for labs.network_mode: service:xmakes one container share another's namespace solocalhosthappens to work. It couples their lifecycles and hides the addressing mistake.
Twelve-Factor calls these peers backing services: attached resources located by configuration (a URL or hostname). You should be able to swap one without changing code.