Networking in Docker: Bridge, veth and Port Mapping

K
Kai··5 min read

Recall Article 2: every container has its own network namespace — an isolated network stack. So how do containers talk to each other and reach the Internet? This article answers that: the kinds of Docker networks, the mechanism underneath, and how to wire containers up correctly.

The network drivers

Per the Docker docs, there are several network drivers, each for a purpose:

  • bridge — the default driver. Creates a private network on a single host; containers in it can talk to each other, and reach outside via NAT.
  • host — drops network isolation: the container uses the host's network stack directly (no separate network namespace). Fast, but you lose isolation.
  • none — no network at all; the container is fully isolated network-wise.
  • overlay — joins multiple Docker hosts so containers on different machines can talk. This is the foundation of Docker Swarm (Article 12).
  • macvlan / ipvlan — make a container appear as a real device on the physical network.

List the existing networks:

docker network ls
NAME      DRIVER
bridge    bridge      ← the default bridge network
host      host
none      null

This article focuses on bridge (the most common network when running on a single machine); overlay is saved for the Swarm part.

The default bridge and the veth mechanism

When Docker starts on Linux, it creates a virtual bridge named docker0 — picture it as a virtual switch. Each container connects to the bridge through a veth pair: a pair of virtual network interfaces joined like the two ends of a cable. One end sits inside the container's network namespace (it shows up as eth0 inside), the other plugs into the docker0 bridge on the host.

        Host
   ┌────────────────────────────────────────────┐
   │   eth0 (to the Internet)                     │
   │     │                                        │
   │   docker0 (virtual bridge = switch)          │
   │     │            │             │             │
   │   veth         veth          veth            │
   │     │            │             │             │
   │  ┌──┴───┐     ┌──┴───┐     ┌───┴──┐          │
   │  │ eth0 │     │ eth0 │     │ eth0 │  ← inside │
   │  │ ctn A│     │ ctn B│     │ ctn C│  container │
   │  └──────┘     └──────┘     └──────┘          │
   └────────────────────────────────────────────┘

Because they all plug into docker0, containers on the same bridge talk to each other over IP. To reach the Internet, traffic from a container goes through the bridge and is then NAT'd (masqueraded) by the host out through eth0 — that's why the docs say containers on the default bridge "access external network services through masquerading" with no extra configuration.

Port publishing: letting the outside into a container

By default a container's ports are only reachable from inside the Docker network. To open one to the outside of the host, use -p (already seen in Article 3):

docker run -d -p 8080:80 nginx:alpine

-p 8080:80 means: traffic to port 8080 on the host is forwarded into port 80 of the container. Underneath, Docker adds a NAT rule (DNAT) to iptables to redirect packets.

   Internet ──► host:8080 ──(iptables DNAT)──► container:80

EXPOSE in a Dockerfile (Article 5) is just documentation stating "this container uses port 80"; it does not open the port on the host by itself. You have to use -p to actually publish it.

User-defined bridge: calling each other by name

This is the most important point when running multiple containers that work together. The Docker docs draw a clear distinction:

  • On the default bridge, containers "access each other by IP address" but "cannot call each other by name".
  • On a user-defined bridge (a network you create yourself), containers "use Docker's internal DNS" and can call each other by container name.

Let's prove it. Create a private network and two containers in it:

docker network create appnet
docker run -d --name svc-a --network appnet alpine sleep 300
docker run --rm --network appnet alpine ping -c 2 svc-a
PING svc-a (172.20.0.2): 56 data bytes
64 bytes from 172.20.0.2: seq=0 ttl=64 time=0.138 ms

The container can ping svc-a by name — Docker's internal DNS resolves the container name to an IP. Now try the same on the default bridge:

docker run -d --name svc-b alpine sleep 300
docker run --rm alpine ping -c 1 svc-b
ping: bad address 'svc-b'

The name doesn't resolve. This is why you should always create a user-defined network when multiple containers need to talk (e.g. a web app calling a database): they just call each other by service name, instead of chasing IPs (which change every time a container restarts). Docker Compose in Article 8 creates such a network for you automatically — one reason Compose is so convenient.

Connecting containers to a network

docker network create appnet           # create a network
docker run --network appnet ...         # run a container straight into the network
docker network connect appnet <ctn>     # attach a running container to the network
docker network inspect appnet           # see which containers are in the network + IP
docker network disconnect appnet <ctn>  # detach it

See who's in the network:

docker network inspect appnet --format '{{range .Containers}}{{.Name}}={{.IPv4Address}} {{end}}'
svc-a=172.20.0.2/16

🧹 Cleanup

docker rm -f svc-a svc-b 2>/dev/null
docker network rm appnet 2>/dev/null
docker network prune        # remove networks no container is using

You can't remove a network that still has containers attached — remove/delete the containers first.

Wrap-up

Containers connect to a virtual bridge (docker0) through a veth pair, talk to each other over it, and reach the Internet via NAT. -p publishes a container port to the host with a DNAT rule. Most important: on a user-defined bridge, containers call each other by name thanks to internal DNS, while the default bridge does not — so create a dedicated network for containers that need to communicate.

By now you have all the pieces: image, container, volume, networking. But running each docker run by hand for a multi-component app (web + database + cache) is tedious and error-prone. Article 8 gathers it all into one file with Docker Compose.