Start a server that binds port 3000. Now start another process that wants the same port. You'll get an error, something like address already in use.
That makes it feel like a computer only has one port 3000, and whoever grabs it first wins. So how does Docker let you run ten containers, each with something bound to port 3000 on the inside, with zero conflicts?
Every process on Linux normally shares a copy of interface, routing table, port table and ARP cache. Two processes fighting over port 3000 are really fighting over that one shared port table. A namespace gives a process its own, interface, routing table, port table and ARP cache. Two processes in two different namespaces can both bind port 3000, because as far as the kernel is concerned, they aren't even on the same network.
(SO_REUSEPORT is a narrower, opt in exception where cooperating processes deliberately share one port. Different problem, not this one.)
So how do you actually make one? One command:
ip netns add ns1
Think of it like spinning up a whole separate machine, just for networking. It gets its own interfaces, its own routing table, its own everything, even though it's still running on the same box as everything else.
Look inside a fresh namespace and there's exactly one interface, lo, and it's down:
ip netns exec ns1 ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
A fresh namespace isn't ready to use out of the box. Even talking to itself over loopback fails until you bring lo up, which is the one step every namespace needs before anything else works:
ip netns exec ns1 ip link set lo up
That's the whole lifecycle. Created, inert, then alive. Getting it talking to anything else needs a cable, and that's a veth pair.
Two namespaces exist now. So how do they actually talk to each other?
A veth pair is two virtual interfaces created together as one unit. Whatever goes in one comes straight out the other, nothing else. Think of it like a phone line running between your room and a friend's. Whatever you say into your end is what comes out their end, and nobody else is on the line.
Smallest useful example: one end sitting in ns1, the other in a second namespace, ns2, nothing else involved yet.
ip netns add ns2
ip link add veth0 type veth peer name veth1
ip link set veth0 netns ns1
ip link set veth1 netns ns2
ip netns exec ns1 ip addr add 10.0.0.1/24 dev veth0
ip netns exec ns2 ip addr add 10.0.0.2/24 dev veth1
ip netns exec ns1 ip link set veth0 up && ip netns exec ns1 ip link set lo up
ip netns exec ns2 ip link set veth1 up && ip netns exec ns2 ip link set lo up
ip netns exec ns1 ping -c1 10.0.0.2
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=0.06 ms
Two isolated network stacks, one cable, and nothing shared between them but that one wire.
But what actually is a network interface? It's just the OS's name for anything that can send and receive packets. lo is one. A physical NIC is one. Each end of a veth pair is one too.
When you first create a namespace, loopback is the only interface it has, and loopback can only talk to itself. It has no way to reach anything outside its own namespace. To send a packet somewhere else, it needs an interface that actually leads out. That's exactly what a veth end is, a network interface just like lo, except instead of looping back to itself, it leads out to whatever's plugged into the other end.
So a veth pair is two interfaces, created together, permanently linked. A namespace can have many interfaces. Interface is the general word, veth is just one specific kind of it.
Making one and giving it an address is two commands:
ip link add veth0 type veth peer name veth1
ip addr add 10.0.0.1/24 dev veth0
That's the same veth0 from section 3, just written out on its own instead of buried inside the full namespace setup.
Two namespaces over one veth cable works fine. But what if you need three? Or four?
Wire every namespace directly to every other one yourself and for n namespaces that's n(n-1)/2 cables you'd have to create and track by hand. Four namespaces fully wired to each other is 6 cables. Five is 10. That's a mesh:
+-----+
| ns1 |
+-----+
/ | \
/ | \
+-----+ | +-----+
| ns5 |-----+-----| ns2 |
+-----+ / \ +-----+
\ / \ /
\ / \ /
\ / \ /
+-----+ +-----+
| ns4 |---| ns3 |
+-----+ +-----+
5 namespaces, fully wired: 10 cables. Add a 6th and that's 5 more.
But what if you could use something like a switch instead?
A Linux bridge is a switch. Not a simulation of one, an actual implementation of the same thing a physical switch does:
Namespace is the isolated network stack, veth pair is the cable, network interface is either end of that cable (or lo, or a NIC), and a bridge is what all those cables plug into so they don't have to plug into each other.
What does that actually look like wired up? Same two namespaces, this time through a bridge instead of a direct cable. (If you're carrying over ns1/ns2 from section 3: delete the old pair first with ip link del veth0, deleting one end deletes both since they're inseparable.) Each namespace's end gets renamed to eth0 this time, since that's the role it's playing now:
ip link add br0 type bridge && ip link set br0 up # the switch
ip link add veth0 type veth peer name veth1 # cable for ns1
ip link set veth0 netns ns1
ip netns exec ns1 ip link set veth0 name eth0
ip link set veth1 master br0 up
ip link add veth2 type veth peer name veth3 # cable for ns2
ip link set veth2 netns ns2
ip netns exec ns2 ip link set veth2 name eth0
ip link set veth3 master br0 up
ip netns exec ns1 ip addr add 10.0.0.1/24 dev eth0 && ip netns exec ns1 ip link set eth0 up && ip netns exec ns1 ip link set lo up
ip netns exec ns2 ip addr add 10.0.0.2/24 dev eth0 && ip netns exec ns2 ip link set eth0 up && ip netns exec ns2 ip link set lo up
ip netns exec ns1 ping -c1 10.0.0.2 # ns1 → br0 → ns2
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=0.05 ms
One ping, and the bridge already knows both MAC addresses:
bridge fdb show br br0
02:1a:4e:7c:9b:01 dev veth1 master br0
02:1a:4e:7c:9b:02 dev veth3 master br0
Same self learning a physical switch does, right there in the output. (exact fdb output varies by kernel, the learning itself is the point.)
If you're on macOS, none of this is native to it. Docker Desktop runs a small Linux VM in the background and does all of this inside that VM instead, your Mac's own kernel never sees a namespace. Windows containers do have a real equivalent though, an NT kernel primitive called a silo.
That's it, that's everything in this post. A namespace, a veth pair, a bridge, that's the whole model. And it's not an approximation of what Docker does, it's literally what dockerd sets up for you every time you run docker run. The default docker0 bridge on your machine is a Linux bridge exactly like br0 here, and every container's eth0 is one end of a veth pair plugged straight into it. You just built Docker's networking model by hand.