Docker Networking Explained: Bridge, Host, and Overlay Networks

September 4, 2026 · 8 min read

Your containers start fine. They can reach the internet. But the web app can't talk to the database, the database can't be reached from your laptop, and nobody — not even you — remembers why -p 8080:80 stopped working after a restart. This is the moment every developer meets Docker networking: not as a concept, but as a wall.

Docker networking is not magic. It is a small set of network drivers with clear rules, plus a built-in DNS server that most connectivity bugs come from ignoring. Once you know which driver to pick and how containers find each other, the wall comes down.

1. The Default Bridge: What You Already Run

When you install Docker, it creates a virtual network called bridge (on Linux; Docker Desktop on macOS and Windows wraps it in a lightweight VM). Any container started without --network attaches to it.

$ docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
3a8f6c1b2d4e   bridge    bridge    local
c2d9e0a1b3f5   host      host      local
7e4a1c9b2d6f   none      null      local

Each container on the bridge gets its own network namespace: a private IP like 172.17.0.2, its own routing table, and its own iptables rules. Outbound traffic is NATed through the host — which is why apt-get works inside a container without any configuration. Inbound traffic only arrives if you explicitly publish a port.

$ docker run -d --name web -p 8080:80 nginx

-p 8080:80 tells Docker to accept connections on the host's port 8080 and forward them to port 80 inside the container. It does not make the container reachable from other containers by its IP — that works already, but only while that IP stays stable.

2. Container-to-Container: DNS Is the Answer

Containers on the default bridge can ping each other by IP, but not by name. This is the trap. Two containers, one running Postgres and one running your API — the API connects to 172.17.0.2, everything works, then you recreate the Postgres container and its IP changes. The API breaks for no obvious reason.

The fix is a user-defined bridge network. Docker runs an embedded DNS server on these networks, and every container is automatically registered by its name (and container ID).

$ docker network create appnet
$ docker run -d --name db --network appnet -e POSTGRES_PASSWORD=secret postgres:16
$ docker run -d --name api --network appnet -p 3000:3000 my-api

# Inside the api container, this now works:
# psql -h db -U postgres

No IPs, no --link legacy flags, no service discovery tooling. The name db resolves to the Postgres container on the same network, and Docker keeps the mapping fresh across restarts. Rule of thumb: always put containers that talk to each other on a user-defined network, never on the default bridge.

The embedded DNS only works on user-defined networks. The default bridge has no automatic DNS, which is why you never see docker run --network default in production examples.

3. Port Publishing vs. Container IPs

A common confusion: "If containers have IPs, why do I need -p?" Because those IPs exist inside a private, host-local bridge. Traffic from outside the host never routes to 172.17.0.2. Port publishing is the deliberate, firewall-like opening of one hole.

$ docker run -d --name web --network appnet -p 8080:80 nginx
$ docker run -d --name metrics --network appnet prom/prometheus   # NOT published

Now web is reachable from your browser at localhost:8080, and Prometheus is reachable from other containers on appnet as http://metrics:9090 — but not from your host. That asymmetry is a feature: it is the container-native way of keeping internal services internal. Only expose what needs exposing.

4. Host Networking: No Isolation, Full Speed

$ docker run -d --name cache --network host redis

With host, the container shares the host's network namespace. No NAT, no bridge hop, no virtual interfaces — the container binds directly to the host's interfaces, and -p is meaningless (and rejected).

This driver buys raw performance and lets the container see host-only services. It costs you everything Docker networking normally guarantees: no per-container IP, no port isolation, no network-level security boundary. A container running with --network host can bind to any port, and any process on the host can see its traffic.

Use it for: latency-sensitive data-plane tools (memcached-style caches, benchmarks), or when a container needs to join a host-only VPN. Avoid it for anything multi-tenant or untrusted.

5. Overlay: Networking Across Hosts

Bridge networks are host-local. If your API runs on host A and the database on host B, a bridge can't connect them. The overlay driver creates a virtual network that spans multiple Docker hosts, encrypting traffic with IPSEC and tunneling packets with VXLAN.

$ docker network create -d overlay --attachable prodnet
$ docker service create --name api --network prodnet my-api

Overlay networks are the native networking layer of Docker Swarm. In Kubernetes you rarely meet them directly — the cluster installs a CNI plugin (Calico, Cilium, Flannel) that implements the same idea: a flat virtual network on top of the physical one, so a pod on any node can reach a pod on any other node by IP.

If you are running a single host, or a multi-host setup already managed by Kubernetes, you will not need overlay. Know that it exists, and know that the DNS + virtual-IP pattern you learned on bridge networks is exactly the abstraction Kubernetes later reuses.

6. The Container Driver: Sidecar Pattern

A fourth driver worth knowing: container. It attaches a new container to another container's network namespace — the same IP, same localhost.

$ docker run -d --name app --network appnet my-api
$ docker run -d --name sidecar --network container:app my-agent

The sidecar sees localhost:8080 and reaches the app. This is how logging, metrics, and proxy sidecars are wired before you hand the problem to Kubernetes' own sidecar pattern. Handy for debugging too: run a throwaway nicolaka/netshoot container on a target's network and get tcpdump, curl, and nslookup without polluting the app image.

$ docker run -it --rm --network container:app nicolaka/netshoot
netshoot # tcpdump -i eth0 port 5432

7. Compose: Networking Without the Pain

Docker Compose wires all this up for you. Every project gets its own network, and every service is reachable by its service name — which is exactly the DNS behavior from section 2, configured for free.

services:
  db:
    image: postgres:16
  api:
    build: .
    ports:
      - "3000:3000"
    depends_on:
      - db

Inside the api container, db:5432 resolves. If you need to split concerns, define extra networks and attach services to only the ones they need — the same least-privilege idea as unpublished ports.

8. Debugging Connectivity Like a Pro

When containers can't talk, work from the inside out:

# 1. Are both containers on the same network?
$ docker network inspect appnet --format '{{range .Containers}}{{.Name}} {{end}}'
db api

# 2. Does DNS resolve?
$ docker exec api getent hosts db
172.18.0.2    db

# 3. Can the port be reached?
$ docker exec api curl -sv http://db:5432

# 4. Inspect the other side
$ docker logs db
$ docker inspect db --format '{{json .NetworkSettings.Ports}}'

The three questions that solve 90% of Docker networking bugs:

Summary

Docker networking, distilled:

Learn to think in names instead of IPs, pick user-defined networks by default, and debug from the container outward. The wall disappears — and so does that whole category of "it worked yesterday" bugs.