Ordered by how often each one turns out to be the answer.
Connecting to localhost from inside a container
CommonThe connection string was written for a development machine where everything ran on one host. Inside a container, localhost is that container's own loopback interface, so the connection is refused by the container itself — nothing ever leaves it.
Confirm
docker exec <app> printenv | grep -iE 'host|url|_addr'
Any localhost or 127.0.0.1 in a connection string pointing at a different service. That is the fault.
Fix
- Use the other container's service name as the hostname:
postgres://user:pass@db:5432/app rather than @localhost:5432. - Keep the container port, not the published host port. Container-to-container traffic goes direct.
# compose
services:
app:
environment:
DATABASE_URL: postgres://user:pass@db:5432/app
db:
image: postgres:16
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
POSTGRES_DB: appdb here is the service name, which Docker's embedded DNS resolves on the shared network.
The containers are not on the same network
CommonName resolution between containers only works on a user-defined network. Containers started separately with docker run land on the default bridge, where they have addresses but no name resolution — so the hostname fails to resolve rather than the connection being refused.
Confirm
docker inspect <app> --format '{{json .NetworkSettings.Networks}}' && docker inspect <db> --format '{{json .NetworkSettings.Networks}}'A network name present in both outputs. If they share none, that is the cause.
Fix
- Put both on the same user-defined network. Compose does this automatically for services in one file, which is why the problem usually appears with plain
docker run. - Distinguish the two error shapes: a name that does not resolve is a network problem, and a connection actively refused means resolution worked and nothing was listening.
docker network create appnet
docker run -d --name db --network appnet -e POSTGRES_PASSWORD=<password> postgres:16
docker run -d --name app --network appnet myapp
The target application is bound to its own loopback
CommonThe name resolves, the network is shared, and the connection is still refused — because the target process is listening on 127.0.0.1 inside its container. Traffic arriving on the container's network interface has nothing accepting it.
Confirm
docker exec <db> sh -c 'ss -tlnp 2>/dev/null || netstat -tlnp'
The Local Address column. 127.0.0.1:5432 is the problem; 0.0.0.0:5432 or *:5432 is correct.
Fix
- Configure the application to bind
0.0.0.0. Many frameworks default to localhost when no host is configured, which is safe on a laptop and wrong in a container. - This is the same three-layer confusion as EXPOSE versus published ports, one level along — the bind address is a property of the application, not of Docker.
The target was not accepting connections yet
OccasionalContainer start order is not readiness. A dependent service can start, resolve the name, and be refused because the database is still initialising. depends_on without a condition waits for the container to be started, not for the application inside it to be ready.
Confirm
docker compose logs db | head -30
Whether the database announced it was ready before or after your application's first failed connection. Compare timestamps.
Fix
- Add a healthcheck to the dependency and use
depends_on: condition: service_healthy. - Better, make the client retry with backoff. Dependencies restart in production too, and start-order configuration does not help then.
services:
db:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
retries: 10
app:
depends_on:
db:
condition: service_healthyConnecting to the published host port instead of the container port
OccasionalWith -p 5433:5432, other containers still reach the database on 5432 — the published mapping is for host traffic. Using 5433 between containers connects to a port nothing is listening on.
Confirm
docker port <db>
Output like 5432/tcp -> 0.0.0.0:5433. The left side is what containers use; the right side is for the host.
Fix
- Use the container port in inter-container connection strings, and the host port only from the host.
The hostname is not a name Docker's DNS knows
OccasionalOn a shared user-defined network, Docker's embedded DNS resolves names you set with --name, Compose service names, network aliases and the short container ID, and nothing else; names Docker generates when you give none do not resolve. A typo, the image name or a hostname from another environment does not resolve even though the network is right.
Confirm
docker network inspect <network> --format '{{range .Containers}}{{.Name}} {{end}}'The exact name you are connecting to. A Compose service name or an alias set with --network-alias also resolves, though only the container name is listed here.
Fix
- Connect using the container name, the Compose service name or a network alias, exactly as written.