DEV Community

Peon Sh
Peon Sh

Posted on Originally published at peon.sh

Docker Networking Explained: Bridges, DNS and Why localhost Breaks

A mental model for Docker networks: how containers find each other, when to publish ports, and the difference between expose and ports.

The mental model: networks are virtual switches
Almost every Docker networking confusion dissolves with one picture: a user-defined bridge network is a virtual switch. Containers attached to it get a private IP and, crucially, a DNS name equal to their container or service name, resolved by Docker’s embedded DNS server. Containers on the same switch reach each other by name on any port; containers on different switches cannot see each other at all; and the host only reaches containers through explicitly published ports.

The "user-defined" qualifier matters: the legacy default bridge (what you get with a bare docker run) does not provide DNS between containers. Always create and use named networks, compose does this automatically per project.

ports vs expose, settled
ports: "8080:3000" binds host port 8080 to the container’s 3000, this is the doorway from the outside world (and the internet, if the firewall allows). Each host port can be bound once
expose: 3000 is documentation only; it changes no behaviour. Containers on a shared network can already reach any port the other container listens on
The production rule: only the reverse proxy publishes ports (80/443); every app and database stays network-internal, this eliminates port conflicts and accidental public databases in one stroke

Why localhost breaks, and what to use instead
Inside a container, localhost is that container’s own loopback interface, not the host, not sibling containers. The two fixes cover 95% of cases: to reach a sibling service, use its network name (postgres, redis, api); to reach something on the host machine, use host.docker.internal (add the extra_hosts mapping on Linux).

The mirror-image bug: an app inside a container binding to 127.0.0.1 is unreachable even with published ports, because the publish forwards to the container’s external interface. Containerized servers must listen on 0.0.0.0.

Linux: make host.docker.internal work

extra_hosts:
- "host.docker.internal:host-gateway"
Enter fullscreen mode Exit fullscreen mode

A platform-shaped example
A typical Peon-managed server runs one shared network (peon) where the proxy, your apps and your databases all live. The proxy publishes 80/443 and routes by hostname; your app reaches its database at postgres-abc:5432 by name; nothing else touches host ports. Two different apps can each listen on "port 3000" internally without any conflict, because host ports are simply not part of the design.

Inspection toolkit
When connectivity confuses you, these four commands answer it empirically:
docker network ls # what switches exist
docker network inspect peon | jq '.[0].Containers' # who is attached
docker exec app getent hosts postgres # does DNS resolve?
docker exec app wget -qO- http://api:3000/health # can I actually reach it?

Top comments (0)