Container Networking Basics for Developers
Container networking can feel like magic: you run a container, map a port, and suddenly your service is reachable. But there is no magic, only a handful of Linux kernel primitives stitched together. Understanding them turns "why can't my containers talk to each other?" from a guessing game into a diagnosis.
Network namespaces: the foundation
A network namespace is an isolated copy of the kernel's networking stack: its own interfaces, routing table, ARP cache, firewall rules, and port space. Every container runs in its own network namespace, which is why two containers can both bind to port 8080 without colliding. The host has its own (default) namespace, separate from all of them.
You can create and inspect namespaces by hand, which is the best way to see that containers are not special:
# Create a namespace and look inside it
sudo ip netns add demo
sudo ip netns exec demo ip addr
# You'll see only a loopback interface, and it's down:
# 1: lo: <LOOPBACK> mtu 65536 state DOWN
A fresh namespace is almost entirely cut off from the world. It has a loopback device (often not even up yet) and nothing else. Everything that follows is about connecting that isolated namespace back to something useful.
veth pairs: the virtual cable
A veth (virtual Ethernet) pair is two linked interfaces that act like a cable: a packet sent into one end comes out the other. The trick is that the two ends can live in different namespaces. Put one end inside the container's namespace and leave the other in the host, and you have a wire bridging the isolation boundary.
# Create a pair: veth0 (host side) <-> veth1 (container side)
sudo ip link add veth0 type veth peer name veth1
# Move one end into the namespace
sudo ip link set veth1 netns demo
# Bring it up and give it an address inside the namespace
sudo ip netns exec demo ip addr add 10.0.0.2/24 dev veth1
sudo ip netns exec demo ip link set veth1 up
sudo ip netns exec demo ip link set lo up
At this point the container namespace has a usable interface, but it is a point-to-point link to a single host interface. That does not scale to many containers. This is where the bridge comes in.
The bridge model
A Linux bridge is a virtual layer-2 switch in the kernel. Instead of giving every container its own dedicated link to the host, you create one bridge and plug the host-side end of each container's veth pair into it. The containers then share an L2 segment and can reach each other directly, the same way machines plugged into a physical switch can.
This is exactly what Docker's default bridge network is. On a typical Docker host you will find a bridge named docker0:
ip addr show docker0
# docker0: ... inet 172.17.0.1/16
# Each running container has a veth attached to it:
bridge link
The bridge interface itself (172.17.0.1 above) acts as the default gateway for every container on that network. When a container sends a packet to an address outside its subnet, it forwards it to the bridge, and the host's routing stack takes over from there.
Port publishing and NAT
Container addresses like 172.17.0.x are private to the host. The outside world cannot route to them, and they are reused on every machine. To make a containerized service reachable from outside, you publish a port:
docker run -d -p 8080:80 nginx
# host port 8080 -> container port 80
Under the hood this is destination NAT (DNAT) implemented with iptables (or nftables) rules. When traffic hits the host on port 8080, a DNAT rule rewrites the destination to the container's IP and port 80, and the reply path rewrites it back. You can see the rules the container runtime installs:
sudo iptables -t nat -L DOCKER -n
# DNAT tcp dpt:8080 to:172.17.0.2:80
Outbound traffic uses the opposite trick: source NAT (masquerading). When a container reaches out to the internet, its private source address is rewritten to the host's address so replies can find their way back. This is the same NAT your home router does, just running inside one Linux host.
Container-to-container communication and DNS
Containers on the same bridge network can reach each other directly by IP, with no NAT involved, because they are on the same L2 segment. But IPs are assigned dynamically and you should not hardcode them. The answer is DNS.
On the default docker0 bridge, automatic name resolution is not provided. On any user-defined bridge network, the runtime runs an embedded DNS server (reachable at 127.0.0.11 inside each container) that resolves container and service names to their current addresses. This is one of the main reasons to always create your own network rather than relying on the default:
docker network create appnet
docker run -d --name db --network appnet postgres
docker run -d --name web --network appnet myapp
# Inside the web container, this just works:
# psql -h db
# "db" resolves to the database container's current IP
This name-based discovery is what lets a compose file or orchestrator reschedule a container to a new IP without breaking its clients. Your application connects to db or redis, not to a number.
Overlay networks for multi-host
Everything so far lives on a single host. Once your containers span multiple machines, a local bridge is not enough: a container on host A has no L2 path to a container on host B. Overlay networks solve this by encapsulating container traffic so it can travel across the existing host network.
The common mechanism is VXLAN, which wraps each container's L2 frame inside a UDP packet (typically on port 4789). Host A's kernel encapsulates the frame, sends it as ordinary UDP to host B, and host B unwraps it and delivers it to the destination container. To the containers it looks like one flat network; to the physical network it looks like normal UDP traffic between hosts.
- A control plane (Swarm's gossip, Kubernetes CNI plugins like Flannel or Cilium) distributes the mapping of container IPs to host addresses.
- The data plane encapsulates and decapsulates packets as they cross host boundaries.
- Overlays add a small overhead: each packet carries extra headers, so the effective MTU is lower (commonly ~1450 bytes). MTU mismatches are a frequent cause of "large requests hang" bugs.
The practical takeaway for developers: on a single host, networking is bridges and NAT; across hosts, there is an encapsulation layer in between, and most multi-host networking problems are really MTU, firewall, or control-plane problems.
Bridge vs host vs none
Docker exposes three built-in network modes that map cleanly onto the concepts above:
- bridge (the default): the container gets its own namespace, a veth into a bridge, a private IP, and NAT-based port publishing. Good isolation, works for almost everything. The small cost is the extra hop through the bridge and NAT rules.
- host (
--network host): the container shares the host's network namespace. No veth, no bridge, no NAT. The container binds directly to host ports, so-pdoes nothing and port conflicts with the host become possible. It is faster and removes a layer, but you lose isolation. Useful for high-throughput networking or tools that need to see the host's real interfaces. - none (
--network none): the container gets a namespace with only loopback and no external connectivity. Use it for batch jobs that should be fully sandboxed from the network, or when you intend to attach your own networking manually.
A quick way to decide: default to bridge (ideally a user-defined one so you get DNS); reach for host only when you have measured a need and accept the loss of isolation; pick none when "no network" is a feature, not an accident.
Takeaway
Container networking is not a separate technology stack; it is a recombination of long-standing Linux primitives. Namespaces provide isolation, veth pairs bridge that isolation, a Linux bridge connects many containers, iptables NAT publishes ports and masquerades egress, an embedded DNS server makes services discoverable by name, and overlays extend the model across hosts. The next time a container cannot reach a service, walk the path: namespace, interface, bridge, routing, NAT, DNS. The break is almost always at one of those well-defined links, and now you know where to look.
← All articles