Skip to main content
DevOps Insightsguides & answers

Docker · troubleshooting

Connection refused between containers

Almost always one of three: localhost used instead of the other container's name, no shared network, or the app listening only on its own loopback. Each has a one-line check below.

First published

New to this? Read what is actually happening first, then come back to the steps.

On this page · 10 sections

Before you start

  • A terminal where you can run docker commands against the host running both containers (root-equivalent on Linux).
  • The names of both containers or Compose services, and the network you expect them to share, from docker network ls and docker inspect.
  • A way to run tools next to the app. Slim images often lack sh, getent, nslookup, ss or netstat; if so, start a throwaway container on the same network, as Docker's docs do with docker run --rm -it --network my-net busybox.
  • The connection string the application uses (host, port, user) with the password removed.
  • The port the target listens on inside its container, from its image documentation or docker port <container>.

Run this first

step 1 of 3
docker exec <app> sh -c 'getent hosts db || nslookup db'

Look for An address means DNS works, so the problem is further along. No answer means a network problem.

Separates resolution from connection immediately, which is the fork in this whole investigation.

Work out which cause you have

A few questions to narrow the list. Every answer ends in a command that confirms or rules the cause out — this cannot see your cluster, so nothing here is a certainty until you have checked.

Narrow it down

0 answered · nothing is sent anywhere

Does the hostname resolve from inside the calling container?

Run docker exec <app> getent hosts <name>. This splits the investigation in two.

Cause space

6 of 6 still possible

  • Connecting to localhost from inside a containerCommon
  • The containers are not on the same networkCommon
  • The target application is bound to its own loopbackCommon
  • The target was not accepting connections yetOccasional
  • Connecting to the published host port instead of the container portOccasional
  • The hostname is not a name Docker's DNS knowsOccasional

Nothing ruled out yet. Answer the question above and the branches your answer eliminates will strike through here.

Check it with a tool

Or diagnose it manually

In this order. The first command usually contains the whole answer.

  1. Step 1

    docker exec <app> sh -c 'getent hosts db || nslookup db'

    Separates resolution from connection immediately, which is the fork in this whole investigation.

    Look for An address means DNS works, so the problem is further along. No answer means a network problem.

  2. Step 2

    docker exec <db> sh -c 'ss -tlnp 2>/dev/null || netstat -tlnp'

    If the name resolves, the next question is whether anything is actually listening on a reachable address.

    Look for 0.0.0.0:<port> rather than 127.0.0.1:<port>.

  3. Step 3

    docker inspect <app> --format '{{json .NetworkSettings.Networks}}'

    Confirms which networks the containers share, which determines whether names can resolve at all.

    Look for A user-defined network appearing in both containers' output.

Every cause, and how to fix it

Ordered by how often each one turns out to be the answer.

Connecting to localhost from inside a container

Common

The 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: app

db here is the service name, which Docker's embedded DNS resolves on the shared network.

The containers are not on the same network

Common

Name 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

Common

The 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

Occasional

Container 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_healthy

Connecting to the published host port instead of the container port

Occasional

With -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

Occasional

On 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.

Understanding it properly

Skip this if you are mid-incident — the working part of the page is above. Worth reading afterwards, because understanding the mechanism is what stops the next one.

In plain terms

Each container has its own private view of the network, including its own localhost. To reach another container, your app has to use a name Docker can look up, on a network both containers share, and the other app has to accept connections from outside its own container. This page checks those three things in order.

Name lookup on the default bridge, from Docker's documentation

Documented, not reproduced: Docker is not installed on this machine. Docker's bridge network guide starts two alpine containers with no --network, so both join the default bridge, attaches to alpine1, and pings alpine2 first by IP address and then by name. The two pings are consecutive steps in the guide, quoted verbatim.

Command

ping -c 2 172.17.0.3
ping -c 2 alpine2

Output · as shown in Docker — Bridge network driver: usage examples

# ping -c 2 172.17.0.3

PING 172.17.0.3 (172.17.0.3): 56 data bytes
64 bytes from 172.17.0.3: seq=0 ttl=64 time=0.086 ms
64 bytes from 172.17.0.3: seq=1 ttl=64 time=0.094 ms

--- 172.17.0.3 ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.086/0.090/0.094 ms

# ping -c 2 alpine2

ping: bad address 'alpine2'

How to read it

  • The ping by IP address gets both replies, so the containers share a network and packets flow between them. Routing and firewalls are not the problem.
  • bad address 'alpine2' is BusyBox ping saying the name did not resolve. Nothing was refused: the lookup failed before any connection was tried. Applications word this differently; Node.js, for example, prints getaddrinfo ENOTFOUND <name>.
  • Docker's explanation is that on the default bridge network, containers can't resolve each other by name. The same guide then creates alpine-net with docker network create, and ping -c 2 alpine2 succeeds there.
  • Only names you set resolve. The guide notes that automatic service discovery does not resolve the random names Docker generates when you start a container without --name.

Words used on this page

localhost inside a container
The container's own loopback address. A connection to localhost or 127.0.0.1 stays inside the container that made it, unless containers deliberately share a network stack.
Default bridge network
The network a container joins when you give no --network. Containers on it reach each other by IP address only, apart from the legacy --link flag.
User-defined network
A network you create with docker network create, or the <project>_default network Compose creates for each project. Containers on it can find each other by name.
Embedded DNS server
Docker's resolver at 127.0.0.11, used by containers on user-defined networks. It answers for container names, service names and aliases on that network and forwards other lookups to the host's DNS servers.
Network alias
An extra name for a container on one network, set with --network-alias, docker network connect --alias or Compose aliases.
Bind address
The address a server listens on inside its container. 0.0.0.0 accepts connections on every interface; 127.0.0.1 accepts only connections from inside the same container.
Published port
A host port mapped to a container port with -p. Containers on the same network ignore it and use the container port; it matters for the host, the outside world and containers on other networks.
What is actually happening

Inside a container, localhost means that container. It does not mean the host or the other container in your compose file, unless you deliberately share a network stack with network_mode: host or network_mode: service:<name>. This is the single most common cause and it catches almost everybody once.

Containers on a user-defined Docker network reach each other by service or container name, which Docker resolves through its embedded DNS. On the default bridge network they cannot resolve each other by name, short of the legacy --link flag.

And even with the right name on the right network, the target application has to be listening on an address that is reachable from outside its own container. A process bound to 127.0.0.1 is unreachable by design.

Published ports play no part in traffic between containers on the same network. -p maps a host port inward; two containers on a shared network talk directly on the container port without any publishing at all.

How to tell this is your problem
WhereWhat you see
Application logsECONNREFUSED 127.0.0.1:5432, or getaddrinfo ENOTFOUND db.
docker network inspect <network>Only one of the two containers listed in Containers.
docker exec <a> ss -tlnpThe target listening on 127.0.0.1:5432 rather than 0.0.0.0:5432.

How to know it is actually fixed

  • docker exec <app> sh -c 'getent hosts db' returns an address.
  • The application connects on start-up without retrying, and its logs show no connection errors.
  • The connection survives restarting the dependency, which confirms the client retries rather than depending on start order.

Stopping it happening again

  • Use service names and container ports in configuration, and treat localhost in a container's config as a bug on sight.
  • Give dependencies healthchecks and depend on health rather than on creation.
  • Make clients retry with backoff. Start-order configuration only helps at start-up, and dependencies fail at other times too.

Security considerations

  • Do not fix this by publishing the database port with -p 5432:5432. Containers on a shared network do not need it, and a port published without an address listens on all host interfaces and, per Docker's documentation, bypasses ufw rules.
  • Give each stack its own user-defined network. Docker's documentation warns that every container started without --network joins the default bridge, where unrelated containers can talk to each other. For back-end services that need no outside access, docker network create --internal restricts external access to the network.
  • Making a server listen on 0.0.0.0 inside its container does not publish it. In the bridge driver's default nat gateway mode, other machines and containers on other networks reach it only through a port you publish. The nat-unprotected mode, kept for legacy behaviour, also lets direct routing reach unpublished ports, so check a network's gateway mode before relying on this.
  • The confirm step docker exec <app> printenv prints whole connection strings, passwords included. Redact them before sharing, and remember that anyone with access to the Docker daemon can read the same values with docker inspect.
  • Do not make localhost work by switching to network_mode: host. Docker's Compose documentation warns that such a container can access all host ports and observe all network traffic on the host.

Sources

Behaviour described here is drawn from official documentation. Where a figure could not be confirmed on an official page it is attributed in the text rather than stated as canonical.

Did this get you to an answer?

No text box on purpose — please do not paste production logs anywhere

Something wrong or out of date? Report it