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:
- Connection refused: something actively rejected the TCP connection (no process listening on that port, or a firewall/proxy rejected it).
- Timeout: packets aren’t reaching the destination or replies aren’t coming back (wrong network, wrong IP/port, DNS issues, routing, firewall rules, or the app is stuck and not responding).
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:
- A network namespace (its own interfaces, routes, iptables view)
- An IP address on a Docker bridge (like
docker0or a user-defined bridge) - NAT rules on the host (for published ports)
Key points:
- Container-to-container traffic on the same user-defined bridge network typically works by container name DNS (Docker’s embedded DNS).
- Published ports (
-p 8080:80) are for host-to-container (and external) traffic. Containers usually should not connect via published ports; they should connect directly to the target container’s internal port on the shared network.
“Connection refused” vs “timeout” at TCP level
- Refused often means the SYN reached the destination, but there was no listening socket (RST returned).
- Timeout often means the SYN never got a response (dropped packets, wrong routing, firewall, wrong IP, or destination not reachable).
Knowing which you have helps you focus.
2) Reproduce the issue with minimal tools
Assume you have:
apicontainer listening on port8000webcontainer trying to callhttp://api:8000/health
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:
Connection refusedOperation timed outCould not resolve hostNo route to host
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:
apidoesn’t resolve: containers not on same user-defined network, or wrong hostname.- You used
localhostby mistake: in a container,localhostrefers to the container itself, not another container.
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
8080is on the host8000is inside the container
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:
- Is the process running?
- Is it listening on the right port?
- Is it bound to
0.0.0.0? - Is the container reachable on the network?
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:
- Wrong IP/hostname (DNS)
- Not on same network
- Firewall rules (host iptables, container iptables, security policies)
- Routing issues
- The server is stuck (accept queue full, app deadlocked)
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:
- You see SYNs arriving but no SYN-ACK: server not listening or firewall dropping.
- You see SYN, SYN-ACK, ACK but then app doesn’t respond: app-layer hang.
- You see nothing arriving: wrong network, wrong destination, DNS issue, routing.
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:
- DROP rules affecting
FORWARDchain - Policies that block inter-bridge traffic
- Custom firewall services (ufw, firewalld) misconfigured
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:
- Services not on the same network
- Wrong port (using host published port internally)
- App binding to
127.0.0.1 - Dependency not ready (timeouts during startup)
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:
apiresolves to theapiservice container IP- If you set
container_name, that name can also be used, but relying on service names is usually better.
“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:
- Add retries in the client
- Add a healthcheck and wait-for-it logic
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)
- From inside
web,localhostisweb. - From inside
api,localhostisapi.
If you need to reach a service running on the Docker host, use:
- On Docker Desktop:
host.docker.internal - On Linux: you may need to add
--add-host=host.docker.internal:host-gateway(Docker 20.10+)
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:
- It does not get a Docker bridge IP.
- Container-to-container via names on a bridge network won’t apply.
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
- Refused → go to Step C
- Timeout → go to Step B
- DNS failure → go to Step D
Step B: Timeout path (network reachability)
- Same network?
docker inspect web --format '{{json .NetworkSettings.Networks}}' docker inspect api --format '{{json .NetworkSettings.Networks}}' - DNS resolves?
getent hosts api - Packets visible?
docker run --rm -it --network <net> nicolaka/netshoot bash tcpdump -i any -nn port 8000 - Host firewall/forwarding?
sysctl net.ipv4.ip_forward sudo iptables -L -n -v
Step C: Refused path (service not listening/binding)
- Is the process running?
docker logs --tail=200 api docker exec -it api ps aux - Is it listening on the right port?
docker exec -it api ss -lntp - Is it bound to 0.0.0.0?
Look for
0.0.0.0:8000not127.0.0.1:8000.
Step D: DNS path (name resolution)
- Are they on a user-defined bridge network?
- Use service/container name.
- Check
/etc/resolv.confinside 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)
- Always use a user-defined bridge network for multi-container apps.
- Use service names for addressing (
http://api:8000), not container IPs. - Ensure servers bind to 0.0.0.0 inside containers.
- Don’t use published host ports for container-to-container communication.
- Add health checks and retry logic for startup dependencies.
- Keep a standard debug image handy (
nicolaka/netshoot) and know how to attach it to networks.
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.