IAnswerable

Debug Docker Networking Between Containers

18 Nov 2025

“Connection refused” between two containers that are supposedly on the same stack is almost always one of three things: wrong hostname, wrong network, or the target service isn’t actually listening where you think it is. Here’s how to find out which, fast, instead of guessing at config changes.

1. Confirm they’re actually on the same network

Containers can only reach each other by service name if they share a Docker network. Compose creates a default network for a project automatically, but custom network configs, multiple Compose files, or a container started outside Compose entirely can break that assumption silently.

docker network ls
docker network inspect <network-name>

The inspect output lists every container attached to that network. If the container you’re trying to reach isn’t in that list, that’s your answer — it’s not unreachable because of a firewall or app bug, it’s unreachable because it’s not on the same network at all.

2. Use the service name, not localhost or the container’s own IP

Inside a container, localhost refers to that container itself, not to a sibling container also part of the stack. This trips people up constantly when moving from local dev (localhost:5432) to Docker (db:5432) — the connection string needs to change, not just the environment.

services:
  app:
    environment:
      DATABASE_URL: postgres://user:pass@db:5432/mydb
  db:
    image: postgres:16

db here is the Compose service name, and Docker’s embedded DNS resolves it automatically for any container on the same network — no manual /etc/hosts entry needed. Don’t hardcode a container’s IP address either; it’s not guaranteed to stay the same across restarts.

3. Test connectivity from inside the container, not from the host

A curl from your host machine tells you nothing about whether containers can reach each other — it only tests whether a port is published to the host. Get inside the calling container instead:

docker compose exec app sh
# then, inside the container:
curl -v http://db:5432
ping db

If ping db fails to resolve at all, it’s a DNS/network membership problem (back to step 1). If it resolves but the connection is refused, DNS is fine and the problem is on the target service’s side — it’s not listening on that port, or not listening on all interfaces.

4. Check what the target is actually listening on

A shockingly common cause: the app inside the target container is bound to 127.0.0.1 instead of 0.0.0.0. Bound to localhost, it only accepts connections from within its own container — invisible to everything else on the network, even containers that can resolve its hostname just fine.

docker compose exec db sh
netstat -tlnp   # or: ss -tlnp

Look at the address column. 127.0.0.1:5432 means only local connections work. 0.0.0.0:5432 means it accepts connections from anywhere reachable, including sibling containers. Most database images default to 0.0.0.0 inside the container, but a custom app config (or one copied from a local-dev setup) can override that without anyone noticing until this exact debugging session.

5. Rule out an internal-only network

Compose supports marking a network internal: true, which blocks it from reaching the outside internet — useful for a database tier that should never make outbound connections. But it’s easy to apply this to a network that also needs outbound access for something else (pulling a package at runtime, hitting an external API), and get a confusing partial-connectivity failure. Check your Compose file’s networks top-level section for internal: true if a container can reach its siblings but not the outside world.

The three-command triage

When two containers can’t talk, this is the decision path the three commands above walk you through:

flowchart TD
    A["Container A can't reach Container B"] --> B{"Same Docker network?"}
    B -- No --> B1["docker network inspect: attach both"]
    B -- Yes --> C{"ping resolves?"}
    C -- No --> C1["DNS / network membership issue"]
    C -- Yes --> D{"Connection refused?"}
    D -- Yes --> D1["Check bind address: 0.0.0.0 vs 127.0.0.1"]
    D -- No --> E["Check for internal: true on the network"]
  

Run these in order and you’ll almost always land on the cause before the fourth command:

docker network inspect <network>          # both containers present?
docker compose exec app ping db           # DNS resolves?
docker compose exec db ss -tlnp           # listening on 0.0.0.0, right port?

The takeaway

Container-to-container networking failures are rarely about Docker’s networking layer being broken — they’re almost always a mismatch between what you assumed (same network, right hostname, service listening broadly) and what’s actually configured. Verify from inside the container, not the host, and check the listening address before you start changing firewall rules that were never the problem.