← Back to Tutorials

How to Debug Connection Refused and Timeouts Between Docker Containers

dockerdocker-networkingcontainerstroubleshootingconnection-refusedtimeoutsdevopsdocker-compose

How to Debug Connection Refused and Timeouts Between Docker Containers

When two Docker containers can’t talk to each other, the symptoms usually fall into two buckets:

This tutorial walks through a systematic approach to diagnose and fix both, using real commands and concrete examples. It assumes Linux Docker Engine, but most steps also apply to Docker Desktop (macOS/Windows) with minor differences.


1) Start with a mental model: how containers connect

Container networking basics (bridge networks)

By default, Docker uses a bridge network. Each container gets:

Key points:

“Connection refused” vs “timeout” at TCP level

Knowing which you have helps you focus.


2) Reproduce the issue with minimal tools

Assume you have:

From inside web, test connectivity:

docker exec -it web sh
# or bash if available:
# docker exec -it web bash

Try:

# DNS + TCP connect test
wget -S -O- http://api:8000/health

If wget isn’t installed, try:

# BusyBox often has wget; Alpine has wget/curl; Debian has curl
curl -v http://api:8000/health

If you have neither, use nc (netcat) or /dev/tcp:

nc -vz api 8000

Or in bash:

timeout 3 bash -c 'cat < /dev/null > /dev/tcp/api/8000' && echo OK || echo FAIL

Record the exact error:

Each points to a different layer.


3) Confirm both containers are on the same Docker network

List networks:

docker network ls

Inspect the network you expect them to share:

docker network inspect mynet

Check that both containers appear under Containers in the output. If not, they are not connected.

Inspect each container’s network attachments:

docker inspect web --format '{{json .NetworkSettings.Networks}}' | jq
docker inspect api --format '{{json .NetworkSettings.Networks}}' | jq

If you don’t have jq, omit it:

docker inspect web --format '{{json .NetworkSettings.Networks}}'

Fix: connect containers to the same network

Create a user-defined bridge network (recommended):

docker network create mynet

Run containers on it:

docker run -d --name api --network mynet myapiimage
docker run -d --name web --network mynet mywebimage

Or connect an existing container:

docker network connect mynet web
docker network connect mynet api

Why user-defined bridge networks matter: Docker provides automatic DNS-based service discovery (container name → IP) on user-defined networks. The default bridge network has more limitations and historically required legacy --link patterns.


4) Verify DNS resolution inside the client container

Inside web:

getent hosts api

If getent is missing:

cat /etc/resolv.conf

Then try:

nslookup api
# or
dig api

Common DNS problems:

Fix: use the service/container name, not localhost

Bad (from web container):

http://localhost:8000

Good:

http://api:8000

If using Docker Compose, the service name is the DNS name by default.


5) Confirm the server is actually listening on the expected port

A huge percentage of “connection refused” issues are simply: the app isn’t listening or is listening on the wrong interface/port.

Check listening sockets inside the server container

Exec into api:

docker exec -it api sh

Then:

ss -lntp

If ss isn’t available:

netstat -lntp

You want to see something like:

LISTEN 0 4096 0.0.0.0:8000 ...

If you see:

127.0.0.1:8000

that means it’s bound only to loopback inside the container. Other containers cannot connect.

Fix: bind to 0.0.0.0, not 127.0.0.1

Examples:

Python / Uvicorn

uvicorn app:app --host 0.0.0.0 --port 8000

Flask

flask run --host=0.0.0.0 --port=8000

Node Ensure your server listens on all interfaces:

server.listen(8000, '0.0.0.0');

Go

http.ListenAndServe(":8000", handler)

Re-test from web:

nc -vz api 8000
curl -v http://api:8000/health

If it changed from “refused” to success, you found the root cause.


6) Validate you’re using the correct port (container port vs published port)

Published ports map host ports to container ports:

docker run -p 8080:8000 myapiimage

From another container on the same network, you should usually connect to:

http://api:8000

Not:

http://api:8080

Because 8080 might not be open inside the container at all.

Check port mappings

On the host:

docker port api

Or:

docker ps --format 'table {{.Names}}\t{{.Ports}}'

7) Distinguish “refused” from “timeout” using packet-level reasoning

If it’s “connection refused”

Focus on:

Check process status:

docker logs --tail=200 api
docker exec -it api ps aux

If the app crashes and restarts, you’ll see it in logs:

docker ps --filter name=api
docker inspect api --format '{{.State.Status}} {{.State.Restarting}} {{.State.ExitCode}}'

If it’s “timeout”

Focus on:

Try a pure TCP SYN test:

nc -vz -w 3 api 8000

If it times out, try ping (ICMP may be blocked, but it’s a quick hint):

ping -c 1 api

If ping works but TCP times out, suspect firewall or app-level issues.


8) Inspect Docker networks and routes in detail

Check IP addresses

On the host:

docker inspect api --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'
docker inspect web --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'

Inside a container:

ip addr
ip route

You should see a default route via the Docker bridge gateway (often 172.x.x.1).

Confirm both are on the same subnet

If api is on 172.18.0.0/16 and web is on 172.19.0.0/16, they won’t reach each other unless you connect them to a shared network.


9) Use a dedicated debug container with networking tools

Many minimal images lack curl, dig, ss, etc. A common technique is to run a toolbox container on the same network:

Option A: netshoot (very handy)

docker run --rm -it --network mynet nicolaka/netshoot bash

Then inside:

dig api
curl -v http://api:8000/health
nc -vz api 8000
ss -lntp
tcpdump -i any port 8000

Option B: alpine + packages

docker run --rm -it --network mynet alpine sh
apk add --no-cache curl bind-tools netcat-openbsd iproute2
curl -v http://api:8000/health

This isolates whether the problem is specific to your web container or general to the network.


10) Debug with tcpdump: see whether packets arrive

If you suspect timeouts or weird behavior, capture traffic.

Capture inside the server container

If tcpdump exists (or in netshoot):

tcpdump -i any -nn host api and port 8000

Better: capture on the server while the client attempts a connection:

On api side:

tcpdump -i any -nn port 8000

On web side, run:

curl -v http://api:8000/health

Interpretation:

Capture on the Docker host

Find the bridge interface:

ip link show | grep -E 'docker0|br-'
docker network inspect mynet --format '{{.Id}}'

Often user-defined networks create br-<id> interfaces. Then:

sudo tcpdump -i br-<id> -nn port 8000

11) Check host firewall / iptables / nftables rules

On many systems, Docker manipulates iptables. If your host firewall is strict, it can interfere.

Quick look at iptables (legacy)

sudo iptables -S
sudo iptables -L -n -v
sudo iptables -t nat -L -n -v

nftables systems

sudo nft list ruleset

Things to look for:

Common fix: allow forwarding (Linux)

Docker typically needs forwarding enabled:

sysctl net.ipv4.ip_forward

Enable temporarily:

sudo sysctl -w net.ipv4.ip_forward=1

Persist via /etc/sysctl.conf or /etc/sysctl.d/*.conf as appropriate.

If using UFW, ensure Docker forwarding is allowed (UFW can block Docker by default depending on config). This is environment-specific; the key is: a timeout from container to container can be a host firewall forwarding issue.


12) Validate Docker Compose configuration (common pitfalls)

If you’re using Docker Compose, the most common causes are:

Inspect networks created by Compose

Compose creates a default network per project. List:

docker network ls | grep <project>

Inspect:

docker network inspect <project>_default

Confirm the DNS name

In Compose, the service name is resolvable:

“depends_on” does not mean “ready”

Compose’s depends_on only orders startup, not readiness. If web starts and immediately calls api, you may see timeouts/refused until api is listening.

Mitigations:

Example health check verification:

docker inspect api --format '{{json .State.Health}}' | jq

13) Debug application-level issues that look like network problems

Sometimes the network is fine, but the server is slow or stuck.

Check resource constraints

On the host:

docker stats

Look for CPU pegged, memory near limit, or OOM kills.

Check if the container was OOM-killed:

docker inspect api --format '{{.State.OOMKilled}}'

Check server backlog / accept queue

Inside server container:

ss -lnt
ss -s

If you see many connections in SYN-RECV or ESTAB and the app isn’t responding, it may be overloaded.

Verify the app actually responds locally

Inside api container:

curl -v http://127.0.0.1:8000/health
curl -v http://0.0.0.0:8000/health

If local works but remote doesn’t, suspect binding or firewall. If local also fails/hangs, it’s the app.


14) Special cases: localhost, host networking, and “host.docker.internal”

Localhost confusion (very common)

If you need to reach a service running on the Docker host, use:

Example:

docker run --rm -it --add-host=host.docker.internal:host-gateway alpine sh
ping -c 1 host.docker.internal

Host network mode

If a container uses --network host, it shares the host network stack. Then:

Check network mode:

docker inspect api --format '{{.HostConfig.NetworkMode}}'

If one container is host and the other is on a bridge, you’ll need a different connectivity plan (often connecting to host IP/localhost on host network, or moving both to the same mode).


15) A structured checklist (fast path)

Use this order to avoid random guessing.

Step A: Identify the symptom precisely

From client container:

curl -v http://api:8000/health
# or
nc -vz -w 3 api 8000

Step B: Timeout path (network reachability)

  1. Same network?
    docker inspect web --format '{{json .NetworkSettings.Networks}}'
    docker inspect api --format '{{json .NetworkSettings.Networks}}'
  2. DNS resolves?
    getent hosts api
  3. Packets visible?
    docker run --rm -it --network <net> nicolaka/netshoot bash
    tcpdump -i any -nn port 8000
  4. Host firewall/forwarding?
    sysctl net.ipv4.ip_forward
    sudo iptables -L -n -v

Step C: Refused path (service not listening/binding)

  1. Is the process running?
    docker logs --tail=200 api
    docker exec -it api ps aux
  2. Is it listening on the right port?
    docker exec -it api ss -lntp
  3. Is it bound to 0.0.0.0? Look for 0.0.0.0:8000 not 127.0.0.1:8000.

Step D: DNS path (name resolution)

  1. Are they on a user-defined bridge network?
  2. Use service/container name.
  3. Check /etc/resolv.conf inside container.

16) Worked example: fix a “connection refused” caused by binding to localhost

Scenario

api is a Python app started like:

uvicorn app:app --host 127.0.0.1 --port 8000

web tries:

curl -v http://api:8000/health

Result:

connect to api port 8000 failed: Connection refused

Diagnosis

Inside api:

docker exec -it api sh -c "ss -lntp | grep 8000 || true"

Output shows:

LISTEN 0 4096 127.0.0.1:8000 ...

Fix

Change to:

uvicorn app:app --host 0.0.0.0 --port 8000

Rebuild/restart container, then re-test:

docker exec -it web sh -c "curl -v http://api:8000/health"

17) Worked example: fix a timeout caused by wrong network

Scenario

api is on mynet_api, web is on mynet_web. DNS might even resolve in one network but not the other. The connection times out.

Diagnosis

On host:

docker inspect api --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}'
docker inspect web --format '{{range $k,$v := .NetworkSettings.Networks}}{{$k}} {{end}}'

They show different networks.

Fix

Connect both to a shared network:

docker network create shared
docker network connect shared api
docker network connect shared web

Now from web:

docker exec -it web sh -c "getent hosts api && nc -vz -w 3 api 8000"

18) Preventing future issues (practical habits)


19) Quick command reference

Inspect and logs

docker ps
docker logs --tail=200 <container>
docker inspect <container>
docker exec -it <container> sh

Networks

docker network ls
docker network inspect <network>
docker network create <network>
docker network connect <network> <container>

Connectivity tests (inside containers)

getent hosts <name>
curl -v http://<name>:<port>/
wget -S -O- http://<name>:<port>/
nc -vz -w 3 <name> <port>
ip addr
ip route
ss -lntp

Packet capture

tcpdump -i any -nn port <port>

Host firewall / forwarding

sysctl net.ipv4.ip_forward
sudo iptables -L -n -v
sudo nft list ruleset

If you share the exact curl -v output from the client container, plus ss -lntp from the server container and docker network inspect of the relevant network, you can usually pinpoint the cause in a few minutes.