Networking in Docker: Bridge, veth and Port Mapping
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.