Docker Networking
Container networking from the kernel up
Docker Networking from First Principles
Docker networking is not magic — it is built entirely on Linux kernel primitives: network namespaces, virtual ethernet (veth) pairs, bridges, and iptables. Understanding these building blocks demystifies every Docker networking behavior.
The Three Kernel Primitives
- Network Namespace — an isolated copy of the network stack (interfaces, routing table, iptables rules, sockets). Each container gets one.
- veth pair — a virtual Ethernet cable with two ends. Packets sent into one end come out the other. Used to connect a container's namespace to the host.
- Bridge (
docker0) — a virtual L2 switch in the host namespace. All container veth endpoints attach here, enabling container-to-container communication.
Default Bridge Architecture
When Docker starts, it creates a Linux bridge interface called docker0 (typically 172.17.0.1/16). Each new container gets:
- Its own network namespace (isolated network stack)
- A veth pair: one end appears as
eth0inside the container, the other end attaches todocker0 - An IP address from the bridge's subnet (e.g.,
172.17.0.2,172.17.0.3, ...) - A default route pointing to the bridge IP (
172.17.0.1)
Topology: docker0 Bridge, veth Pairs, and Containers
Note
Note: Container-to-container traffic on the same bridge never leaves the host kernel. The bridge forwards frames at L2 — like a physical switch connecting VMs on the same hypervisor.
Outbound Traffic: Container to Internet
Containers sit behind NAT. When a container sends traffic to the internet, the packet traverses several stages in the host kernel:
Container eth0
Packet leaves container namespace via veth
docker0 bridge
Bridge receives frame, routes to host stack
iptables MASQUERADE
SNAT: rewrite src IP to host IP
Host eth0
Physical interface sends to gateway
Internet
Reply comes back, conntrack maps it
The MASQUERADE Rule
Docker inserts this iptables rule in the nat table's POSTROUTING chain:
$ iptables -t nat -L POSTROUTING -n -v --line-numbers
Chain POSTROUTING (policy ACCEPT)
num target prot opt source destination
1 MASQUERADE all -- 172.17.0.0/16 0.0.0.0/0 ! -o docker0
The rule in iptables syntax:
# Source NAT for container traffic leaving the host
-t nat -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADEBreaking Down the Rule
-t nat— operate on the NAT table-A POSTROUTING— append to POSTROUTING chain (after routing decision)-s 172.17.0.0/16— match packets from the Docker subnet! -o docker0— only when the packet is NOT going out the docker0 interface (i.e., heading to the outside world, not to other containers)-j MASQUERADE— SNAT the source IP to the outgoing interface's IP (likeSNATbut dynamically selects the IP)
Tip
Tip: MASQUERADE vs SNAT — MASQUERADE dynamically discovers the outgoing IP (useful for DHCP). SNAT requires a static IP. Docker uses MASQUERADE because it doesn't know the host's external IP at rule creation time.
Inbound Traffic: Port Mapping (-p 8080:80)
Port mapping exposes a container service on a host port. Under the hood, Docker installs two iptables rules:
1. DNAT Rule (nat table, DOCKER chain)
# Redirect host port 8080 to container 172.17.0.2 port 80
-t nat -A DOCKER -p tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80This rewrites the destination address. Traffic arriving at host:8080 gets rewritten to 172.17.0.2:80 before routing.
2. ACCEPT Rule (filter table, DOCKER chain)
# Allow forwarded traffic to reach the container
-A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp --dport 80 -j ACCEPTThis allows the DNAT'd traffic through the FORWARD chain to reach the container.
External client
Connects to host:8080
PREROUTING/DNAT
Rewrite dst to 172.17.0.2:80
FORWARD/ACCEPT
Filter allows the packet
docker0 bridge
Routes to veth pair
Container :80
Receives the connection
Warning
Warning: Port mapping with -p 8080:80 binds to 0.0.0.0:8080 by default, meaning it's accessible from ALL interfaces — including the public internet. Use -p 127.0.0.1:8080:80 to bind only to localhost.
Network Drivers
Docker supports multiple networking modes via pluggable drivers. Each driver addresses a different use case and makes different trade-offs between isolation, performance, and complexity.
| Driver | Isolation | Performance | Multi-Host | Use Case |
|---|---|---|---|---|
bridge |
Per-network namespace | Good (NAT overhead) | No | Default for standalone containers |
host |
None (shares host namespace) | Best (no NAT, no veth) | No | Performance-critical services, monitoring agents |
none |
Complete (no networking) | N/A | No | Security-sensitive batch jobs, offline processing |
macvlan |
L2 isolation (own MAC) | Excellent (no bridge) | Same L2 segment | Legacy apps needing LAN presence, DHCP clients |
overlay |
Per-network namespace | Moderate (VXLAN encap) | Yes (Swarm) | Multi-host container communication |
Driver Details
bridge (default)
- Creates an isolated Linux bridge
- NAT for outbound traffic (MASQUERADE)
- Port mapping (
-p) for inbound - Containers on same bridge can communicate directly
- Containers on different bridges are isolated
$ docker run --network bridge nginxhost
- Container shares the host's network namespace
- No isolation — container sees all host interfaces
- No NAT, no port mapping needed — bind directly to host ports
- Best performance (zero networking overhead)
- Port conflicts with host and other
--network hostcontainers
$ docker run --network host nginxnone
- Container gets only the loopback interface
- No external network access whatsoever
- Use case: security-sensitive offline processing, generating secrets
$ docker run --network none alpine ip addr
# Only shows lo (127.0.0.1)macvlan
- Container gets its own MAC address on the physical network
- Appears as a real device on the LAN (visible to other machines, can get DHCP lease)
- No bridge, no NAT — direct L2 access
- Requires promiscuous mode on the parent interface
- Cannot communicate with the host (by design — use macvlan sub-interface on host to fix)
$ docker network create -d macvlan \
--subnet=192.168.1.0/24 \
--gateway=192.168.1.1 \
-o parent=eth0 my_macvlanoverlay (Docker Swarm)
- VXLAN-based multi-host networking — containers on different Docker hosts can communicate as if on the same L2 network
- Encapsulates L2 frames in UDP (port 4789) between hosts
- Requires a key-value store for coordination (built into Swarm, or external etcd/consul)
- Each overlay network has its own VXLAN Network Identifier (VNI)
- Conceptually the precursor to Kubernetes overlay networking (Flannel VXLAN mode)
$ docker network create -d overlay --attachable my_overlayNote
Note: ipvlan is similar to macvlan but shares the parent's MAC address. It avoids promiscuous mode requirements and works better in cloud environments where MAC filtering is enforced (e.g., AWS).
Docker's Embedded DNS Server
Docker runs an embedded DNS server at 127.0.0.11 inside each container on user-defined networks. This enables service discovery by container name.
- No automatic DNS resolution
- Must use IP addresses or legacy
--linkflag /etc/resolv.confpoints to host's DNS- Cannot resolve container names
# This FAILS on default bridge:
$ docker exec app ping db
ping: db: Name or service not known- Automatic DNS resolution by container name
/etc/resolv.confpoints to127.0.0.11- Also resolves network aliases
- DNS round-robin for multiple containers with same alias
# This WORKS on user-defined bridge:
$ docker exec app ping db
PING db (172.18.0.3): 56 data bytes
64 bytes from 172.18.0.3: seq=0 ...How Embedded DNS Works
- Docker injects
nameserver 127.0.0.11into each container's/etc/resolv.conf - The DNS server runs in the Docker daemon, not in the container
- It intercepts DNS queries via iptables DNAT rules inside the container's namespace
- Container name queries are resolved locally; all others are forwarded to the host's configured DNS
- Supports round-robin load balancing when multiple containers share a network alias
User-Defined Bridges vs Default Bridge
| Feature | Default Bridge (docker0) |
User-Defined Bridge |
|---|---|---|
| DNS resolution | No (IP only, or --link) |
Yes (automatic by container name) |
| Isolation | All containers share one bridge | Containers only see their own network |
| Connect/disconnect live | No (must recreate container) | Yes (docker network connect/disconnect) |
| Configurable subnet | Limited (daemon.json only) |
Yes (at creation time) |
| Container linking | Legacy --link (deprecated) |
Not needed — DNS handles it |
| Recommended | No (legacy) | Yes (always prefer this) |
$ docker network create my-app-net
a1b2c3d4e5f6...
$ docker run -d --name db --network my-app-net postgres
$ docker run -d --name app --network my-app-net -e DB_HOST=db my-app
# app can now resolve "db" to the postgres container's IP
Tip
Tip: A container can be attached to multiple user-defined networks simultaneously, letting it communicate with different groups of services while maintaining isolation between them. Use docker network connect second-net container-name.
Docker Compose Networking
Docker Compose automatically creates a dedicated bridge network for each project. This is one of its biggest ergonomic wins — zero networking configuration needed for most apps.
Default Behavior
- Compose creates a network named
<project-name>_default - All services in the
docker-compose.ymljoin this network - Service names (the YAML keys) become DNS hostnames
- Services can reach each other by name — no IP configuration needed
# docker-compose.yml
services:
web:
image: nginx
ports:
- "8080:80"
api:
image: my-api
environment:
- DB_HOST=db # resolves via DNS
db:
image: postgres:16
# All three services can reach each other as "web", "api", "db"
# Compose creates network: myproject_defaultCustom Network Configuration
services:
web:
image: nginx
networks:
- frontend
api:
image: my-api
networks:
- frontend
- backend # api bridges both networks
db:
image: postgres:16
networks:
- backend
networks:
frontend:
driver: bridge
backend:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/16
# web can talk to api (frontend network)
# api can talk to db (backend network)
# web CANNOT talk to db (different networks, no route)Debugging Docker Networking
Essential commands for inspecting and troubleshooting container networking:
# List all Docker networks
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
a1b2c3d4e5f6 bridge bridge local
f6e5d4c3b2a1 host host local
1a2b3c4d5e6f none null local
9f8e7d6c5b4a myproject_default bridge local
# Inspect a network — shows subnet, gateway, connected containers
$ docker network inspect bridge
[{
"Name": "bridge",
"IPAM": { "Config": [{ "Subnet": "172.17.0.0/16", "Gateway": "172.17.0.1" }] },
"Containers": {
"abc123...": { "Name": "web", "IPv4Address": "172.17.0.2/16" }
}
}]
# Inspect networking from inside a container
$ docker exec -it web ip addr
1: lo: <LOOPBACK,UP> mtu 65536
inet 127.0.0.1/8 scope host lo
2: eth0@if7: <BROADCAST,MULTICAST,UP> mtu 1500
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
# Check routing table inside a container
$ docker exec -it web ip route
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 scope link src 172.17.0.2
# Check DNS resolution
$ docker exec -it web cat /etc/resolv.conf
nameserver 127.0.0.11
options ndots:0
# View Docker's iptables rules on the host
$ iptables -t nat -L -n -v
$ iptables -L DOCKER -n -v
# See veth pairs — match host-side veth to container
$ ip link show type veth
$ brctl show docker0 # (or: bridge link show)
Tip
Tip: To find which veth on the host corresponds to a container's eth0, run docker exec container cat /sys/class/net/eth0/iflink — the number matches the host-side veth's interface index from ip link.
Warning
Warning: docker network inspect shows the current state. If you suspect stale iptables rules or phantom interfaces, check with iptables -L -n -v and ip link directly on the host. Docker doesn't always clean up perfectly after crashes.