Container networking tends to work fine right up until it doesn’t, and when it breaks the failure mode is rarely obvious. A service can’t reach another service, a port that’s supposedly published refuses connections, or DNS resolves inside one container but not another. This post walks through a systematic approach to diagnosing these problems, using the tools Docker gives you out of the box.
Start with the network topology
Before touching logs or packet captures, get a clear picture of what networks exist and which containers are attached to them. Docker Compose creates a default bridge network per project, but multi-network setups are common once you start isolating a database tier from a public-facing tier.
docker network ls
docker network inspect my-app_default
The inspect output lists every container attached to the network along with its internal IP address. If a container you expect to see isn’t listed, that’s usually the actual bug — not a firewall rule or a misconfigured DNS entry.
A quick mental model helps here:
| Network driver | Typical use case | Containers reach each other by |
|---|---|---|
bridge (default) | Single-host multi-container apps | Container name (via embedded DNS) |
host | Performance-sensitive services, no isolation | localhost |
overlay | Multi-host Swarm/Kubernetes-style clusters | Service name via overlay DNS |
none | Fully isolated containers | Not at all |
Most “container A can’t talk to container B” issues on a single host come down to the two containers not being on the same bridge network at all.
Check DNS resolution from inside the container
Docker’s embedded DNS server resolves container and service names automatically, but only for containers on a shared user-defined network. The default bridge network (literally named bridge) does not support name-based resolution — only IP addresses work there. This trips people up constantly when they expect ping web to work outside of Compose.
Exec into the container and test resolution directly:
docker exec -it my-app_api_1 sh -c "getent hosts db || nslookup db"
If that fails but docker network inspect shows both containers on the same network, check whether the network is user-defined:
docker inspect my-app_api_1 --format '{{json .NetworkSettings.Networks}}' | jq
If the container is only attached to the default bridge, add both services to a user-defined network in your Compose file:
services:
api:
image: my-app/api
networks:
- backend
db:
image: postgres:16
networks:
- backend
networks:
backend:
driver: bridge
Verify port publishing, not just container listening
A common source of confusion: a service listening inside the container doesn’t automatically mean it’s reachable from the host. EXPOSE in a Dockerfile is documentation, not enforcement — it doesn’t publish anything. Only -p (or ports: in Compose) actually maps a container port to the host.
docker ps --format 'table {{.Names}}\t{{.Ports}}'
If the PORTS column is empty or missing the port you expect, the fix is in your run command or Compose file:
docker run -d -p 8080:80 --name web nginx
Also check what the process inside the container is actually bound to. A service bound to 127.0.0.1 inside the container is unreachable from outside it, even with correct port publishing, because the container’s loopback interface is separate from the host’s. The fix is almost always to bind to 0.0.0.0 in the application config.
docker exec -it web sh -c "netstat -tlnp || ss -tlnp"
Trace the actual path with a throwaway debug container
When name resolution and port publishing both look correct but traffic still doesn’t get through, attach a minimal debugging container to the same network rather than adding tools to your production image:
docker run --rm -it --network my-app_backend nicolaka/netshoot bash
netshoot bundles curl, dig, tcpdump, iproute2, and friends, and staying out of your production images with these tools keeps them smaller. From inside it:
dig db
curl -v http://api:3000/health
tcpdump -i eth0 host db
This isolates the problem to either the network layer (packets aren’t arriving) or the application layer (packets arrive, but the service rejects or mishandles them).
Inspect iptables when things get stranger
Docker manages its own iptables rules for NAT and port forwarding, and on Linux hosts these can conflict with rules from other tools (firewalls, VPN clients, Kubernetes’ kube-proxy on the same host). If a port is published, DNS resolves, and the target process is bound correctly — but connections still time out — check the NAT table:
sudo iptables -t nat -L DOCKER -n -v
Look for a DNAT rule matching your published port. If it’s missing, Docker’s networking state may be out of sync with the running containers, which usually means a restart of the Docker daemon:
sudo systemctl restart docker
This is a blunt instrument, so treat it as a last resort after the network- and application-layer checks above have been ruled out — it briefly interrupts every running container on the host.
A minimal checklist
When a container networking issue comes up, work through these in order rather than guessing:
- Confirm both containers are attached to the same user-defined network (
docker network inspect). - Confirm DNS resolves the target name from inside the source container (
getent hosts,dig). - Confirm the target process is bound to
0.0.0.0, not127.0.0.1, inside its container. - Confirm the port is actually published if you’re connecting from the host (
docker ps). - Use a throwaway
netshootcontainer to isolate network-layer from application-layer failures. - Only after that, look at
iptablesand the Docker daemon itself.
Most container networking bugs are one of the first four items. The remaining two are worth knowing about, but reaching for them first usually means skipping past the actual cause.