The core idea
Every container has its own network stack: its own network card, its own address, its own list of where to send traffic, and its own ports. In plain words, it behaves like a separate small computer plugged into a network. Docker connects that private network to the outside world with three pieces of ordinary Linux networking:
- 01
veth pair
A virtual cable. One end is the container's eth0, the other end sits on the host.
- 02
Bridge
A virtual switch on the host. Every container's cable plugs into it, so they can talk.
- 03
NAT out
Traffic leaving for the internet is rewritten to come from the host's IP.
- 04
NAT in (-p)
Published ports rewrite incoming traffic to the container's IP and port.
The networks Docker gives you
$ docker network lsNETWORK ID NAME DRIVER SCOPEed8a6de1e4b5 bridge bridge local... host host local... none null local$ docker network inspect bridge -f '{{range .IPAM.Config}}subnet={{.Subnet}} gateway={{.Gateway}}{{end}}'subnet=172.17.0.0/16 gateway=172.17.0.1
Output trimmed. Containers you start without --network join bridge, the default bridge, and get an address
from 172.17.0.0/16. host and none are special modes, covered in 05.03 Host and none networks.
The view from inside
$ docker run --rm alpine sh -c 'ip -4 addr show eth0 | grep inet; ip route; cat /etc/resolv.conf'inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0default via 172.17.0.1 dev eth0172.17.0.0/16 dev eth0 scope link src 172.17.0.2nameserver 192.168.65.7
eth0is the container's end of the virtual cable, with its own IP.default via 172.17.0.1: anything not on the local subnet goes to the bridge's gateway, the host.resolv.confon the default bridge is copied from the host (here, Docker Desktop's resolver). Remember this; it changes on user-defined networks.
Talking to each other, and out
Containers on the same bridge can reach each other by IP. Outbound traffic is NAT'ed through the host, so the internet just works:
$ docker run -d --name web1 nginx:alpine$ docker inspect -f '{{.NetworkSettings.Networks.bridge.IPAddress}}' web1172.17.0.2$ docker run --rm alpine wget -qO- http://172.17.0.2 | grep -o '<title>.*</title>'<title>Welcome to nginx!</title>$ docker run --rm alpine wget -qO- http://web1wget: bad address 'web1'$ docker run --rm alpine wget -qO- http://example.com | grep -o '<title>.*</title>'<title>Example Domain</title>
By IP it works; by name it doesn't. The default bridge has no DNS for container names. And IPs are handed out fresh every time a container starts, so hard-coding them breaks on the next restart. That's why real setups use user-defined networks (05.02 User-defined bridges and DNS).
Where -p fits in
Publishing -p 8080:80 adds a rule on the host: "traffic arriving on port 8080 → rewrite to this container's IP,
port 80". That's the NAT-in step. It's also why containers on the same network never need -p to reach each other:
they talk directly over the bridge.
Two containers on the default bridge need to talk. The API connects to the database at 172.17.0.3, which works. What goes wrong next week?
Start two alpine sleep 300 containers. For each, find its IP with docker inspect and from inside with ip addr. Ping one from the other by IP, then try by name. Stop and start one and check whether its IP changed.
STUCK? ▸
No ip command: that's an image without iproute2 or busybox. Use alpine or nicolaka/netshoot for exploring.
Recap
Interview questions
Explain how Docker's default bridge networking works.