Docker Networking Explained the Way It Finally Clicked for Me
Bridge networks, port publishing, and why localhost lies to you inside a container — the mental model that made container networking stop being magic.
For months I did not understand why my container couldn't reach a service on localhost:5432. The database was right there — I could hit it from the host. Inside the container, connection refused. Every Stack Overflow answer said "use the service name," and I nodded without understanding why.
Then someone drew it on a whiteboard, and it took four minutes. Here's that drawing, in text form.
Every container gets its own network namespace
This is the whole insight. A network namespace is a private copy of the network stack: its own interfaces, its own ports, its own localhost. When a container says "localhost," it means itself — not your laptop, not the host VM, not the other container.
So localhost:5432 from inside your app container is asking the app container itself if it's listening on 5432. It isn't. Connection refused. The database, meanwhile, lives in a different namespace with a different IP.
The bridge: where containers get their IPs
By default, user-defined and default networks use a bridge — a virtual switch on the host. Containers attached to it get IPs from a private range (typically 172.17.0.0/16 for the default bridge), talk to each other directly at the IP level, and reach the outside world through the host's NAT.
Two kinds of bridge matter:
- Default bridge — containers see each other by IP only, no DNS by name, and
--linkis deprecated. Avoid it for anything real. - User-defined bridge — created with
docker network create app-net. Containers get automatic DNS resolution by name: servicedbis reachable atdb, containercacheatcache. This is the "use the service name" advice, and now you know why it works — Docker's embedded DNS answers the name inside the network namespace.
Publishing ports: how the host gets involved
-p 8080:80 means: listen on the host's 8080, forward to the container's 80. Docker sets up the forwarding (iptables/nftables rules, plus a userland proxy in some configurations) so external traffic can find a container that lives behind NAT.
The direction is easy to mix up: host:container. I've typed it backwards enough times that I now say it out loud. "Publish eight-oh-eighty to eighty."
Host networking (--network host) skips all of this — the container shares the host's namespace directly, ports are the host's ports, localhost is the host's localhost. Blazing fast, zero isolation between container and host ports, Linux-only. Useful for performance-sensitive agents; wrong default for apps.
Compose makes the topology boring (in a good way)
services:
api:
build: .
networks: [app-net]
ports:
- "8080:8080"
depends_on:
- db
db:
image: postgres:16
networks: [app-net]
volumes:
- db-data:/var/lib/postgresql/data
networks:
app-net:
volumes:
db-data:
In this file, api connects to the database at db:5432 — hostname db, Docker DNS resolves it, no ports published for the database at all (which is the correct posture: your database does not need to be reachable from your laptop unless you're actively developing against it).
depends_on controls start order, not readiness — the classic trap. Compose waits for the container to start, not for Postgres to accept connections. Your API will still race the database on first boot; add a wait-for-it, a healthcheck with depends_on: condition: service_healthy, or retry logic in the app. Healthchecks are the right answer.
Debugging commands I reach for
docker network inspect app-net # who's on the network, what IPs
docker exec -it api sh # get inside the namespace
docker exec api getent hosts db # does DNS resolve from here?
docker exec api nc -zv db 5432 # can we actually connect?
Half of "networking is broken" is DNS, and getent hosts splits DNS from connectivity in one command. The other half is firewall rules on the host — iptables -L -n -v (or nft) shows where the packets are dying.
The model, one more time
Containers = private network namespaces. Bridges = shared wire with DNS on user-defined networks. Port publishing = forwarding from host into the namespace. localhost inside a container = that container, always.
Once those four sentences land, Docker networking stops being a checklist of random flags and becomes topology you can draw before you type a single -p.