Docker & ContainersVII · NetworkingNetwork troubleshooting
Network troubleshooting — packet flow and tools
What you'll learn
- Trace a packet through the host network stack
- Use ip, ss, tcpdump, nsenter, nft, conntrack to diagnose
- Follow a systematic troubleshooting tree
Prerequisites
Verified against Docker Engine 29.x · Docker Engine 28.x · Docker Compose 2.x · containerd 2.x · runc 1.2.x · BuildKit 0.20+ · Linux kernel 5.15+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-09
“Container cannot be reached from outside” is the most common Docker networking incident. The diagnostic tree is short but you must follow it in order. Skip a step and you will spend hours on the wrong layer.
The troubleshooting tree
flowchart TD
Start[Container unreachable] --> Q1{Host can ping container?}
Q1 -- no --> P1[Check bridge / veth / iptables]
Q1 -- yes --> Q2{Container can ping external host?}
Q2 -- no --> P2[Check default gateway / DNS / outbound iptables]
Q2 -- yes --> Q3{External host can reach host:published port?}
Q3 -- no --> P3[Check host firewall / port publishing / proxy]
Q3 -- yes --> Q4{External host can reach container?}
Q4 -- no --> P4[Check DNAT rules / conntrack]
Q4 -- yes --> Done[Working - investigate further if flaky]
Always start with docker inspect to confirm the container’s
network configuration, then move outward.
Step 0 — Confirm the basics
docker ps
docker inspect CONTAINER --format '{{json .NetworkSettings}}'
docker network ls
docker network inspect NETWORK
Is the container on the network you expect? Does it have the IP you expect? Is the right port published?
Step 1 — From the host, can you reach the container?
# Ping the container's IP
PIP=$(docker inspect --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' CONTAINER)
ping -c 3 $PIP
# TCP connect to a published port
curl -v http://localhost:8080
# Or from inside a peer container
docker run --rm --network NETWORK alpine nc -zv web 80
If this fails:
- The container is not running (
docker psshows Exited). - The container is in a different network namespace than you think.
- The bridge or iptables rules are broken (a daemon misconfig).
Step 2 — Inspect the host’s networking
# Show bridges
ip link show type bridge
# Show veth interfaces
ip -o link show | grep veth
# Show iptables DNAT
iptables -t nat -L DOCKER -n -v
# Show conntrack entries
conntrack -L | grep 172.17.0
A working DNAT chain has a rule for the published port.
Step 3 — Trace the packet
# On the host, watch for incoming traffic to the published port
tcpdump -i any -n 'port 8080'
# Inside the container, watch for traffic on the application port
PID=$(docker inspect --format '{{.State.Pid}}' CONTAINER)
nsenter -t $PID -n tcpdump -i eth0 -n 'port 80'
You should see:
- The SYN arriving on the host’s external interface.
- The SYN appearing inside the container’s eth0.
- The SYN-ACK returning from the container to the host.
- The SYN-ACK leaving the host’s external interface.
If the SYN arrives on the host but not inside the container, the DNAT or bridge is broken. If the SYN arrives inside the container but no SYN-ACK is returned, the application is not listening or is hung.
CONTAINER=web
PID=$(docker inspect --format '{{.State.Pid}}' "$CONTAINER")
sudo nsenter -t "$PID" -n ss -tlnpState Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 511 127.0.0.1:8080 0.0.0.0:* users:(("node",pid=4127,fd=20))Illustrative output
Read the Local Address column and nothing else:
| Local Address | Reachable from the host via a published port? |
|---|---|
0.0.0.0:8080 | Yes — bound to every interface in the namespace. |
[::]:8080 | Yes, and on IPv4 too on most hosts, via the v4-mapped path. |
172.19.0.4:8080 | Yes — bound to the container’s bridge address specifically. |
127.0.0.1:8080 | No. Container loopback only. |
| nothing listening | No — the application never bound at all; read its logs. |
If you cannot use nsenter, the same answer is available with
docker exec "$CONTAINER" ss -tlnp on images that ship ss, or
docker exec "$CONTAINER" cat /proc/net/tcp on ones that do not —
in that file the local address is hex and little-endian, so loopback
appears as 0100007F.
CONTAINER=web
docker port "$CONTAINER"
PID=$(docker inspect --format '{{.State.Pid}}' "$CONTAINER")
CIP=$(docker inspect --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$CONTAINER")
curl -sS -o /dev/null -w 'direct to container: %{http_code}\n' "http://$CIP:8080/" || true
curl -sS -o /dev/null -w 'via published port: %{http_code}\n' 'http://127.0.0.1:8080/' || true8080/tcp -> 0.0.0.0:8080
curl: (7) Failed to connect to 172.19.0.4 port 8080: Connection refused
via published port: 000Illustrative output
Connecting directly to the container’s IP is the decisive test,
because it takes DNAT, docker-proxy and the host firewall entirely
out of the path. If that fails too, the problem is inside the
container and no networking change will fix it. If it succeeds while
the published port fails, then — and only then — the problem really
is in the publishing path.
Step 4 — Inspect from inside the container
# Inside the container's network namespace
PID=$(docker inspect --format '{{.State.Pid}}' CONTAINER)
nsenter -t $PID -n ss -tlnp
nsenter -t $PID -n ss -tnp # established connections
nsenter -t $PID -n ip route
nsenter -t $PID -n ip addr
nsenter -t $PID -n iptables -t nat -L
What is the application actually listening on? What does the container think its IP and gateway are?
Step 5 — DNS
docker exec CONTAINER cat /etc/resolv.conf
docker exec CONTAINER nslookup web
docker exec CONTAINER getent hosts web
DNS failures look like TCP failures but are diagnosed by nslookup.
Tools you should know
| Tool | Purpose |
|---|---|
ip link | Show interfaces |
ip addr | Show addresses |
ip route | Show routing table |
ss -tlnp | Listening TCP sockets |
ss -tnp | Established TCP sockets |
tcpdump | Packet capture |
nsenter -t PID -n CMD | Run a command in the container’s network namespace |
nft list ruleset | nftables ruleset |
iptables -t nat -L | iptables NAT table |
conntrack -L | Connection tracking entries |
bridge link show | Linux bridge port list |
Knowledge check
Knowledge check · 5 questions
Q1. You publish port 8080:80 with `docker run -p 8080:80 nginx`. From the host, `curl http://localhost:8080` fails with connection refused. What is the first thing to check?
Q2. Which tools help diagnose container networking? Select all that apply.
Q3. Name one technique for inspecting a container's network namespace from the host.
Q4. `nsenter -t $PID -n ss -tlnp` inside a container shows `LISTEN 127.0.0.1:8080`. The port is published with -p 8080:8080 and the DNAT rule is present. What fixes it?
Q5. Which single test takes DNAT, docker-proxy and the host firewall out of the path at once?
Passing score: 75%. Answers are checked in this browser.