Skip to main content
RunBook Academy

Docker & ContainersVII · NetworkingNetwork troubleshooting

Network troubleshooting — packet flow and tools

Advanced⏱ ~28 min

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

Not yet marked complete on this device.

“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 ps shows 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.

Read-only / Safewhat is the application really bound to?
CONTAINER=web
PID=$(docker inspect --format '{{.State.Pid}}' "$CONTAINER")
sudo nsenter -t "$PID" -n ss -tlnp
State  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 AddressReachable from the host via a published port?
0.0.0.0:8080Yes — bound to every interface in the namespace.
[::]:8080Yes, and on IPv4 too on most hosts, via the v4-mapped path.
172.19.0.4:8080Yes — bound to the container’s bridge address specifically.
127.0.0.1:8080No. Container loopback only.
nothing listeningNo — 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.

Read-only / Safeconfirm the mapping separately from the listener
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/' || true
8080/tcp -> 0.0.0.0:8080
curl: (7) Failed to connect to 172.19.0.4 port 8080: Connection refused
via published port: 000

Illustrative 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

ToolPurpose
ip linkShow interfaces
ip addrShow addresses
ip routeShow routing table
ss -tlnpListening TCP sockets
ss -tnpEstablished TCP sockets
tcpdumpPacket capture
nsenter -t PID -n CMDRun a command in the container’s network namespace
nft list rulesetnftables ruleset
iptables -t nat -Liptables NAT table
conntrack -LConnection tracking entries
bridge link showLinux bridge port list

Knowledge check

Knowledge check · 5 questions

  1. 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?

  2. Q2. Which tools help diagnose container networking? Select all that apply.

  3. Q3. Name one technique for inspecting a container's network namespace from the host.

  4. 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?

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