Docker networking is built on standard Linux kernel features. Nothing Docker-specific in the kernel — it’s all primitives that existed before Docker. Understanding them makes networking behaviour predictable rather than magical.
The Core Building Blocks
1. Virtual Ethernet Pairs (veth)
When a container starts, the kernel creates a veth pair — two virtual network interfaces connected like opposite ends of a pipe. Whatever goes in one end comes out the other:
container end: eth0 ↔ host end: vethXXXXXX
The container end (eth0) is moved into the container’s net namespace, so the container sees it as its only NIC. The host end stays in the host namespace.
2. Linux Bridge (docker0)
The host ends of all veth pairs are plugged into a Linux bridge — docker0 by default. A bridge is a virtual Layer 2 switch implemented entirely in the kernel. It forwards Ethernet frames between attached interfaces based on MAC addresses, just like a physical switch.
docker0 (bridge, 172.17.0.1)
/ \ \
vethAAA vethBBB vethCCC
| | |
container1 container2 container3
(172.17.0.2) (172.17.0.3) (172.17.0.4)
Containers on the same bridge can reach each other through it. The bridge’s own IP (172.17.0.1) becomes the default gateway for containers.
3. iptables / netfilter
The kernel’s netfilter framework (managed via iptables) handles two jobs:
- NAT (masquerade) — when a container sends traffic to the internet, iptables rewrites the source IP from the container’s private IP to the host’s public IP. Replies are translated back. Standard SNAT, exactly like a home router.
- Port mapping —
-p 8080:80is a DNAT rule: packets arriving at the host on port 8080 get their destination rewritten to172.17.0.x:80before hitting the bridge.
Docker writes these iptables rules automatically when containers start.
4. Network Namespaces
Each container gets its own net namespace — its own private view of the network stack: interfaces, routing table, iptables rules, ports. Port 80 inside one container doesn’t conflict with port 80 inside another because they’re in completely separate namespaces.
A Packet’s Journey
Container to internet:
container1 eth0
→ vethAAA (veth pair)
→ docker0 (bridge)
→ host routing table
→ iptables MASQUERADE (SNAT: rewrite src IP)
→ host's physical NIC
→ internet
Host to container on mapped port 8080:
host:8080
→ iptables DNAT (rewrite dst to 172.17.0.2:80)
→ docker0 (bridge)
→ vethAAA (veth pair)
→ container1 eth0:80
The Network Modes at OS Level
| Mode | OS mechanism |
|---|---|
| Bridge (default) | veth pair + Linux bridge + iptables NAT |
| Host | No net namespace created — container shares host’s network stack directly |
| Overlay | veth + bridge + VXLAN tunnel (encapsulates L2 frames in UDP across hosts) |
| None | Net namespace created but nothing attached — loopback only |
Host mode is the simplest: the container doesn’t get a net namespace. Processes bind directly to the host’s interfaces. Zero isolation, zero overhead. Useful when you need maximum network performance or a tool that must see the host’s real interfaces.
Overlay adds VXLAN: a kernel feature that wraps Ethernet frames in UDP and ships them between hosts, making containers on different machines appear to be on the same L2 network. This is what Docker Swarm uses.
DNS Resolution Inside Containers
When you create a custom network (or use Docker Compose), Docker runs an embedded DNS server at 127.0.0.11 inside each container’s network namespace. Container names and service names resolve to their current IP addresses automatically — no /etc/hosts maintenance required.
This is why in a Compose file you can use DB_HOST=db and it just works: the db service name resolves to whatever IP the db container currently has.
The default docker0 bridge does not have this DNS — containers can reach each other only by IP. This is one of the main reasons to always use custom networks (or Compose, which creates one automatically).
# Custom network — names resolve
docker network create mynet
docker run -d --name db --network mynet postgres:15
docker run --network mynet alpine ping db # works
# Default bridge — names do not resolve
docker run -d --name db postgres:15
docker run alpine ping db # failsPort Mapping
Port mapping is the mechanism for making a container’s port accessible from outside the host.
docker run -p 8080:80 nginx # all interfaces on host, port 8080 → container port 80
docker run -p 127.0.0.1:8080:80 nginx # loopback only — not accessible from outside the hostBinding to all interfaces (0.0.0.0) is the default and makes the port accessible from any network the host is on. Binding to 127.0.0.1 restricts it to the host machine — useful in production where a reverse proxy (nginx, Caddy) sits in front and the app port shouldn’t be publicly reachable.
One sentence: a Docker network is a Linux bridge with veth pairs connecting container net namespaces to it, and iptables rules handling NAT and port mapping — all standard kernel primitives, no Docker-specific kernel code involved.