SDP Clouds
← All posts
Containers·3 min read

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 --link is deprecated. Avoid it for anything real.
  • User-defined bridge — created with docker network create app-net. Containers get automatic DNS resolution by name: service db is reachable at db, container cache at cache. 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.

#docker#networking#containers#devops