Containers often feel like magic, and container networking can seem like a higher form of black magic. A process starts, gets its own IP address, can reach the internet, and talks to other containers. All without us touching low-level networking.
But nothing magical is happening.
Under the hood, container networking is built from a small set of Linux primitives: network namespaces, virtual Ethernet devices (veth), routing tables and NAT. In this write-up, we’ll build that setup manually to see exactly what Docker and other container runtimes are doing on our behalf.
The Four Linux Primitives Behind Container Networking
Docker and container runtimes, in general, did not invent container networking. They assemble a few Linux kernel features that have existed for years.
Container networking is built on four core primitives:
- Network namespaces
- Virtual Ethernet (veth) devices
- Routing tables
- NAT (iptables)
Network Namespaces: The Separate Network Worlds
A network namespace is a fully independent instance of the Linux networking stack. When a process runs inside the namespace, it gets its own:
- Network interfaces
- Routing table
- ARP table
- Firewall rules (iptables)
- Loopback device
It cannot see the host’s networking interfaces, and the host cannot see its internal ones either (unless explicitly connected). From a networking perspective, it’s as if that process is running on a completely different machine.
Container network isolation is simply a process placed inside its own network namespace.
That’s the foundation of container networking.
Virtual Ethernet (veth) Pairs: The Wire that connects the Worlds
Now we have two separate network worlds: the host and the namespace. They need a wire between them. That wire is a veth pair.
A veth pair is created as two connected virtual interfaces:
veth-A <==== virtual cable ====> veth-B
Anything that enters one end comes out the other.
Typically:
- One end stays in the host
- The other end is moved into the container’s namespace
This is how packets physically move between the container and the host network stack.
A veth pair is just an Ethernet cable, except both ends live inside the kernel.
Routing: How Packets Know Where to Go
Interfaces move packets, but routing tables decide where packets should be sent. Each namespace maintains its own routing table.
When a process sends traffic:
- Kernel checks the destination IP
- Looks up the routing table
- Chooses the correct interface
Without a default route, a namespace can only talk to its own subnet. So if a container wants internet access, it needs a rule like:
“Anything not local -> send to the host”
This rule makes the host act as a gateway.
NAT: How Containers Reach the Internet
Containers usually use private IP addresses (like 10.x.x.x or 172.x.x.x). The internet doesn’t know how to route traffic back to those.
So the host performs Network Address Translation (NAT)
When packets leave the host:
- The container’s source IP is replaced with the host’s IP
- Return traffic comes back to the host
- The kernel maps it back to the original container
This is done using an iptables MASQUERADE rule
Containers don’t have public IPs. They borrow the host’s identity when talking to the outside world.
Putting It Together
At this point, container networking is no longer mysterious:
| Problem | Linux Primitive That Solves It |
|---|---|
| Isolation | Network namespace |
| Connection to host | veth pair |
| Traffic direction | Routing table |
| Internet access | NAT (iptables) |
Docker is simply automating the creation and wiring of these pieces.
In the upcoming sections, we’ll build this entire setup manually (the same way a container runtime does).
Building Container Networking From Scratch
Now we’ll manually build the same network setup a container runtime creates automatically.
We will:
- Create a network namespace
- Connect it to the host using a veth pair
- Assign IP addresses
- Add routing
- Enable IP forwarding
- Add NAT
- Test connectivity
1. Create a Network Namespace
This namespace will act as our “container”.
sudo ip netns add myns
We can run commands inside it using:
sudo ip netns exec myns bash
At this point, the namespace exists. But it has no network connectivity except a downed loopback interface. Try listing the available links inside the namespace.
sudo ip netns exec myns ip link list
You’ll see the loopback interface.
2. Create a veth Pair (Virtual Cable)
We’ll now create a virtual Ethernet cable between the host and the namespace.
sudo ip link add veth-host type veth peer name veth-ns
This creates:
veth-host <====> veth-ns
Move one end into the namespace:
sudo ip link set veth-ns netns myns
Now:
| Side | Interface |
|---|---|
| Host | veth-host |
| Namespace | veth-ns |
3. Assign IP Addresses
The two ends must be on the same subnet.
Host side:
sudo ip addr add 10.10.0.1/24 dev veth-host
sudo ip link set veth-host up
Namespace side:
sudo ip netns exec myns ip addr add 10.10.0.2/24 dev veth-ns
sudo ip netns exec myns ip link set veth-ns up
sudo ip netns exec myns ip link set lo up
Now the host and namespace can talk at Layer 3.
Why bring up lo? Because every network stack expects a working loopback interface. This ensures localhost works correctly inside the namespace.
4. Add a Default Route Inside the Namespace
Right now the namespace only knows its local subnet. We make the host act as a gateway:
sudo ip netns exec myns ip route add default via 10.10.0.1
Now any traffic destined outside 10.10.0.0/24 goes to the host. The namespace now has a default gateway.
5. Enable IP Forwarding on the Host
Linux drops forwarded packets by default.
sudo sysctl -w net.ipv4.ip_forward=1
The host can now behave like a router. This change is temporary and will reset after reboot.
6. Add NAT (Masquerading)
The namespace IP is private. We must rewrite packets leaving the host.
Replace wlo1 with your internet-facing interface.
sudo iptables -t nat -A POSTROUTING -s 10.10.0.0/24 -o wlo1 -j MASQUERADE
This makes outbound traffic appear as if it originates from the host. This is the same mechanism home routers use to allow multiple devices to share one public IP.
7. Allow Forwarded Traffic
sudo iptables -A FORWARD -i veth-host -o wlo1 -j ACCEPT
sudo iptables -A FORWARD -o veth-host -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
These rules allow traffic to leave and return. The first rule allows outbound traffic from the namespace to the external interface. The second rule allows return traffic for established connections, so responses from the internet can flow back into the namespace.
8. Test Connectivity
Namespace → Host
sudo ip netns exec myns ping 10.10.0.1
Namespace → Internet
sudo ip netns exec myns ping 8.8.8.8
If this works, you have built container-style networking manually.
What We Just Built
myns process
↓
veth-ns
↓
veth-host
↓
Host routing table
↓
iptables NAT (MASQUERADE)
↓
wlo1
↓
Router → Internet
At this point, the namespace can reach the host and the internet. That means we have successfully built container-style networking using nothing but Linux kernel primitives.
Let’s look at what actually happened.
- We created a separate network world using a network namespace.
- We connected that world to the host using a veth pair.
- We told the namespace where to send unknown traffic using a default route.
- We allowed the host to forward packets by enabling IP forwarding.
- We let the namespace talk to the internet by adding NAT (MASQUERADE) rules.
That is the core of container networking.
When you run: docker run nginx
Docker performs these same steps automatically. It creates a namespace for the container, wires it to the host network, adds routing rules, and inserts iptables rules so traffic can flow in and out. What feels like a high-level abstraction is just automation around standard Linux networking features.
What we built is the simplest container networking model: one isolated network stack using the host as its gateway.
A container does not get special networking.
It is simply a process inside a separate network namespace, connected to the host using a virtual Ethernet cable.
The host acts as a router and performs NAT so the container can reach the internet.
Everything else in container networking builds on this exact pattern.
Real container platforms add another layer on top of this.
Instead of connecting each namespace directly to the host, they attach multiple namespaces to a Linux bridge, allowing containers to talk to each other as if they were on the same switch. That bridge is what Docker exposes as docker0.
We’ll build that in the next part.