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.

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:

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.

containersnetworkinglinuxdockerdevops
← All articles