The container needs to talk. Right now it has no network at all — or rather, it has the host's, which is worse: it can bind the host's ports and see the host's interfaces.
Fixing that takes three pieces. A network namespace to hold an isolated stack. A veth pair — a virtual cable with a plug at each end. And a bridge, which is a software switch on the host that the container plugs into.
Not a filter, not a virtual interface: a complete, independent set of interfaces, routing tables, ARP caches, iptables rules, and port space.
sudo ip netns add netns_my-container
sudo ip netns exec netns_my-container ip addr
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
One interface, lo, and it is down. Nothing else — no ethernet, no routes, no way out. ip netns exec NAME COMMAND is how you run anything inside it.
Note that lo starts down. A container that cannot reach its own 127.0.0.1 has usually hit exactly this, and the fix is one line further down.
Two containers are in separate network namespaces. Both bind port 8080. What happens?
A bridge is a virtual layer-2 switch living in the host's kernel. Give it an address and it also becomes the containers' gateway:
sudo ip link add name bridge0 type bridge
sudo ip addr add 10.0.0.1/24 dev bridge0
sudo ip link set bridge0 up
That is docker0 on any machine running Docker, under a different name. Run ip addr show docker0 and you will see the same shape — a bridge holding 172.17.0.1/16, acting as the default gateway for every container on the default network.
A veth is created as a pair, and it behaves exactly like a physical cable: whatever goes in one end comes out the other. One end stays on the host and plugs into the bridge; the other is handed to the container.
# The virtual cable: veth_host <-> veth_cont
sudo ip link add dev veth_host type veth peer name veth_cont
# Plug the host end into the bridge and switch it on
sudo ip link set veth_host master bridge0
sudo ip link set veth_host up
This one is a recording — the commands are real, but nothing is running here. Type them into your own shell to follow along.
The @veth_cont in the name is the kernel telling you which interface is the other end of this cable.
This is the step where the isolation actually happens:
sudo ip link set veth_cont netns netns_my-container
veth_cont now disappears from the host's ip link output entirely. An interface belongs to exactly one network namespace, and it has just moved. The cable still runs between the two — that is the point — but the host can no longer configure or see that end.
Every remaining command runs inside the namespace, via ip netns exec:
sudo ip netns exec netns_my-container ip link set dev lo up
sudo ip netns exec netns_my-container ip addr add 10.0.0.2/24 dev veth_cont
sudo ip netns exec netns_my-container ip link set dev veth_cont up
sudo ip netns exec netns_my-container ip route add default via 10.0.0.1
In order: bring up loopback, give the container end an address on the bridge's subnet, bring the interface up, and add a default route pointing at the bridge.
This one is a recording — the commands are real, but nothing is running here. Type them into your own shell to follow along.
The container can reach the host. It cannot reach the internet — 10.0.0.0/24 is a private range that no router on the way out will carry a reply back to. Fixing that is the next lesson.
Why does the container reach 10.0.0.1 but not 8.8.8.8, even with a default route configured?
Name resolution has nothing to do with the network namespace. It is /etc/resolv.conf inside the container's filesystem:
sudo sh -c "echo 'nameserver 8.8.8.8' > ./btrfs-mount/my-container/etc/resolv.conf"
Two isolation mechanisms, one problem — which is why "the container has network but can't resolve anything" is such a common failure. The network namespace is set up correctly; the file in the root filesystem is empty or missing.
Everything above is what docker network create does, with three additions.
Automation and lifecycle. A veth pair per container, named from the container ID, created on start and destroyed on stop. IP addresses handed out from the bridge's subnet by an internal IPAM allocator instead of typed by hand.
An embedded DNS server. On a user-defined network, Docker runs a resolver at 127.0.0.11 inside each container and writes that address into /etc/resolv.conf. It resolves container names to their current addresses, which is what makes postgres:5432 work in a compose file. This is also why containers on the default bridge cannot resolve each other by name — the embedded DNS only serves user-defined networks, and that one difference accounts for a large share of "it works in compose but not with plain docker run".
Port publishing. -p 8080:80 is a DNAT rule in the host's nat table rewriting the destination of inbound packets to the container's address, plus a userland proxy process as a fallback. sudo iptables -t nat -L DOCKER shows the rules for every published port on the machine.
None of it is a different mechanism. It is bookkeeping on top of veth pairs, a bridge, and iptables.
Run ip addr inside the network namespace netns_web.
Next up: NAT — the three iptables rules that let a private address reach the public internet.
