DEV Community

Bar Dror
Bar Dror

Posted on Originally published at codebeneath.com

How Container Networking Works: From veth to the Bridge

Originally published on Code Beneath.

When you run a Docker container, you assume the network just works. A container gets an IP, you can reach it from the host, and you don't think about the mechanics. But understanding how docker container networking works is essential if you want to debug network issues, build custom networks, or understand why your container can't reach a service. The magic is not magic at all: it is virtual ethernet devices, kernel bridges, network namespaces, and some iptables rules. This article strips away the Docker abstraction and builds a bridge and veth pair by hand using only the ip command, so you see exactly what Docker does for you.

The Problem: Why Containers Need Special Networking

A container is not a separate physical machine. It is a Linux process with its own network namespace: its own set of network interfaces, routing table, and firewall rules. The kernel uses namespaces to isolate processes. A container's process thinks it owns eth0, but that interface does not exist on the host. The host has eth0 or wlan0, not the container.

So how does a packet from inside a container reach the outside world? How does the host reach the container? Docker solves this with virtual ethernet devices and a bridge. The bridge acts like a network switch: it connects the host to one side and the container to the other, and it forwards packets between them. Each container gets a virtual ethernet pair, one half in the container namespace and one half connected to the bridge on the host.

Virtual Ethernet Pairs (veth): The Tunnel Between Namespaces

A veth pair is a Linux kernel mechanism that creates two virtual network interfaces permanently connected to each other. Send a packet to one side, it emerges from the other side. Think of it as a pipe between two network namespaces.

Let us create a veth pair by hand:

#!/bin/bash
set -e

# Create a veth pair
# One side: veth-host, one side: veth-container
ip link add veth-host type veth peer name veth-container

# Check that both exist
ip link show veth-host
ip link show veth-container

# Bring both up
ip link set veth-host up
ip link set veth-container up

# Verify status
ip link show | grep veth
Enter fullscreen mode Exit fullscreen mode

Run this script and you will see two new network interfaces. They are not connected to anything yet, just hanging there. If you send a packet to veth-host, the kernel forwards it to veth-container and vice versa. But since there is no namespace setup yet, both are on the host namespace.

Network Namespaces: Isolating the Container

A container's network isolation comes from a network namespace. The kernel separates each namespace so a process in a namespace sees only its own network interfaces. Create a namespace, move veth-container into it, and now the container process sees only veth-container, not the host's real interfaces.

#!/bin/bash
set -e

# Create a network namespace called "container1"
ip netns add container1

# Move veth-container into the container1 namespace
ip link set veth-container netns container1

# Verify: veth-container is no longer visible on the host
ip link show veth-container 2>&1 || echo "veth-container not on host (expected)"

# But it is visible from inside the namespace
ip netns exec container1 ip link show

# We can also run a shell inside the namespace
# ip netns exec container1 bash
Enter fullscreen mode Exit fullscreen mode

Once veth-container is in the container1 namespace, the host kernel still manages it, but the container process cannot see veth-host or eth0. This is the isolation. The container thinks it has a brand new veth-container interface that is its only link to the outside.

The Bridge: The Network Switch

Now we need a way for the host and container to communicate. Enter the bridge: a virtual network switch that lives on the host. A bridge interface on Linux can connect multiple physical or virtual interfaces and forward frames between them, just like a hardware switch.

#!/bin/bash
set -e

# Create a bridge
ip link add br0 type bridge

# Bring the bridge up
ip link set br0 up

# Connect veth-host to the bridge
ip link set veth-host master br0

# Give the bridge an IP address (the gateway for containers)
ip addr add 172.17.0.1/16 dev br0

# Verify
ip link show br0
ip addr show br0
Enter fullscreen mode Exit fullscreen mode

The bridge is now up and has an IP address on the 172.17.0.0/16 subnet. veth-host is connected to it. When the host sends to 172.17.0.x, it goes to the bridge. The bridge, seeing veth-host, forwards it into veth-container. The container receives it on its veth-container interface.

Assigning IPs to the Container and Host Side

The bridge has 172.17.0.1. Now we assign the container side an IP on the same subnet. Inside the container namespace, we also need a default route pointing back to the bridge (the gateway).

#!/bin/bash
set -e

# Inside container1 namespace: assign IP to veth-container
ip netns exec container1 ip addr add 172.17.0.2/16 dev veth-container

# Bring up the interface inside the namespace
ip netns exec container1 ip link set veth-container up

# Set default route inside the container to the bridge (host's veth-host IP is not visible, but gateway on bridge is)
ip netns exec container1 ip route add default via 172.17.0.1 dev veth-container

# Verify from inside the container
ip netns exec container1 ip addr show
ip netns exec container1 ip route show
Enter fullscreen mode Exit fullscreen mode

Now the container has 172.17.0.2 on its veth-container interface, and its default route points to 172.17.0.1 (the bridge). The host has 172.17.0.1 on br0. They are on the same subnet.

Testing Connectivity

Let us verify that packets flow between the host and container. The simplest test is a ping from the container to the gateway.

#!/bin/bash
set -e

# Ping from container to the bridge (gateway)
echo "Pinging gateway from container:"
ip netns exec container1 ping -c 2 172.17.0.1

# Ping from host to the container
echo "Pinging container from host:"
ping -c 2 172.17.0.2

# Verify packet flow with tcpdump (optional, requires another terminal)
# sudo tcpdump -i veth-host -n icmp
Enter fullscreen mode Exit fullscreen mode

If both pings work, packets are flowing through the veth pair and the bridge. The host sees 172.17.0.2 on the bridge. The container sees 172.17.0.1 as its gateway. This is the core of container networking.

Reaching External Networks

So far, the container can reach the host on the bridge. But can the container reach the outside world (for example, 8.8.8.8)? No, not yet. The host knows about the 172.17.0.0/16 subnet (it has a route to it via br0), but external networks do not know how to reach 172.17.0.2. The container's packets leave the bridge but hit a dead end.

Docker solves this with NAT (network address translation). The host adds an iptables rule to masquerade outgoing packets from the container, rewriting the source IP from 172.17.0.2 to the host's real IP. Replies come back to the host, and iptables rewrites them back to 172.17.0.2. The container never knows.

#!/bin/bash
set -e

# Enable IP forwarding on the host
echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward

# Add a masquerade rule for the bridge subnet
sudo iptables -t nat -A POSTROUTING -s 172.17.0.0/16 ! -o br0 -j MASQUERADE

# Now test external connectivity from the container
ip netns exec container1 ping -c 2 8.8.8.8 || echo "External ping may fail if no default route outside"

# Check the iptables rule
sudo iptables -t nat -L -n
Enter fullscreen mode Exit fullscreen mode

With masquerade, outgoing packets from 172.17.0.2 are rewritten to the host's IP. The outside world replies to the host, and the host rewrites the response back to 172.17.0.2. The container receives it as if it was talking directly to 8.8.8.8.

Inbound Traffic: Exposing Container Ports

A container runs a web server on port 8080. You want to access it from outside. The container listens on 172.17.0.2:8080, but the outside world cannot reach 172.17.0.2. Docker adds a port mapping: the host listens on 0.0.0.0:8080 and forwards incoming connections to 172.17.0.2:8080.

This is also done with iptables rules in the PREROUTING and POSTROUTING chains:

#!/bin/bash
set -e

# Map host port 8080 to container port 8080
sudo iptables -t nat -A PREROUTING -d 172.17.0.1 -p tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:8080
sudo iptables -t nat -A POSTROUTING -d 172.17.0.2 -p tcp --dport 8080 -j MASQUERADE

# Or use docker-proxy, which Docker does internally
# docker-proxy -container-ip 172.17.0.2 -container-port 8080 -host-port 0.0.0.0:8080

# To test, run a simple HTTP server inside the container
# ip netns exec container1 python3 -m http.server 8080 &
# curl http://172.17.0.1:8080 (from host)
Enter fullscreen mode Exit fullscreen mode

The first rule intercepts incoming traffic to the bridge IP on port 8080 and rewrites the destination to the container IP. The second rule masquerades the reply so the container thinks it is talking to the outside.

What Docker Automates

Docker does all of the above automatically when you run a container. Here is what it does under the hood:

  1. Creates a veth pair
  2. Moves one side into the container's network namespace
  3. Creates or reuses a bridge (docker0, or a user-defined bridge network)
  4. Connects the host side of the veth to the bridge
  5. Assigns IPs from the bridge subnet to both the host side and container side
  6. Sets up the default route inside the container
  7. Adds iptables rules for masquerading and port mapping
  8. Ensures IP forwarding is enabled on the host

The primitives are simple: veth pairs, bridges, network namespaces, and iptables. Everything else is orchestration.

Comparison: Host, Bridge, and Overlay Networks

Mode Isolation Performance Use Case
Host None; container shares host namespace Minimal overhead; direct access to host NICs High-performance services, monitoring agents
Bridge (default) Full; container in separate namespace One veth pair per container, iptables overhead Typical single-host containerized apps
Overlay Full; encrypted tunnel between hosts Additional encapsulation, slight latency Multi-host Docker Swarm or Kubernetes

How to Inspect the Real Thing in Docker

Start a real Docker container and inspect the networking:

#!/bin/bash
set -e

# Start a container in the background
docker run -d --name test-container alpine sleep 1000

# Get the container's PID
PID=$(docker inspect -f '{{.State.Pid}}' test-container)
echo "Container PID: $PID"

# Inspect the veth pair from the container side
# The container's interface is usually eth0
docker exec test-container ip link show

# Get the container IP
docker exec test-container ip addr show

# Find the host-side veth by looking at the bridge
docker network inspect bridge

# Look at iptables rules Docker created
sudo iptables -t nat -L -n

# Ping the container from the host
docker exec test-container ip addr show eth0 | grep "inet " | awk '{print $2}' | cut -d/ -f1 | xargs ping -c 2

# Clean up
docker stop test-container
docker rm test-container
Enter fullscreen mode Exit fullscreen mode

Socket and TCP/UDP Fundamentals

Understanding veth pairs and bridges is the foundation, but it sits on top of sockets and the TCP/UDP layer. If you are debugging network issues at the container level, you also need to understand how TCP and UDP sockets work and how the kernel routes packets through the network stack. A container's process makes a socket call, the kernel forwards it through the veth pair, the bridge, and eventually out the host's real NIC. If a packet does not arrive, the problem could be at any layer: the veth pair, the bridge, the routing table, or the firewall rules.

Debugging Tips

When container networking breaks:

  1. Check that veth pairs exist: ip link show | grep veth
  2. Verify the bridge exists and the veth is attached: ip link show br0 and brctl show
  3. Confirm IPs are assigned: ip addr show br0, docker inspect <container>
  4. Test connectivity with ping from the host to the container IP
  5. Check iptables rules: sudo iptables -t nat -L -n
  6. Enable IP forwarding if it is disabled: echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward
  7. Use tcpdump to see packet flow: sudo tcpdump -i br0 -n

Common Mistakes

IP forwarding disabled. By default, Linux does not forward packets between interfaces. If you set up a bridge and try to ping from the container to an external host, you will see packets arrive at the bridge but no response. Enable forwarding with echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward.

Forgot masquerade rules. Without iptables masquerading, outgoing packets from the container carry the source IP 172.17.0.2, which the outside world does not know how to route back to. The host receives no reply. Add the masquerade rule.

Assigning the same IP to multiple containers. If two containers get the same IP on the same bridge, the kernel will not know which veth pair to use, and packets will be dropped or routed incorrectly. Each container needs a unique IP on the bridge subnet.

Confusing the bridge IP with the host IP. The bridge is a separate interface with its own IP (172.17.0.1). It is not the host's primary NIC. The host might have eth0 with 192.168.1.100, and br0 with 172.17.0.1. They are independent. The container talks to the bridge, not directly to eth0.

Tools and Resources

If you want to explore this further, use tools like strace, tcpdump, and iptables to watch the kernel do its work. strace shows every system call a process makes, including socket creation and bind. tcpdump shows every frame on every interface. iptables lets you see and modify the rules the kernel uses to route traffic.

FAQ

Why does Docker use bridges and not just connect containers directly?

A bridge scales. One bridge can connect dozens of containers. The bridge acts as a central hub, so you do not need a unique veth pair for every pair of containers. Also, the bridge sits on the host, so it can apply global rules like masquerading and port mapping in one place.

Can I put a container on the host network?

Yes. Docker supports --network host, which runs the container in the host's network namespace. The container sees the host's eth0, listens on the host's ports, and has no isolation. It is fast but not isolated, so it is only safe for trusted workloads.

What is the difference between a bridge and overlay network?

A bridge is single-host only. All containers on a bridge must be on the same physical machine. An overlay network spans multiple machines: it uses a tunnel (usually VXLAN) to connect containers on different hosts. Docker Swarm and Kubernetes use overlay networks for multi-host deployments.

Does every container get its own veth pair?

Yes, when using bridge networking. Each container has one veth pair, with the container side in the container's namespace and the host side connected to the bridge. If a container joins multiple networks, it gets multiple veth pairs.

What happens if I delete the bridge while containers are running?

The containers lose connectivity. If you try to delete a bridge that has veth pairs attached, the kernel will refuse. You must disconnect the veth pairs first, then delete the bridge. Docker handles this cleanup automatically.

Can I manually create a Docker network?

Yes. docker network create my-net creates a user-defined bridge network. Docker assigns it a subnet and creates a new bridge interface. Containers on this network can communicate with each other by name (if they use the embedded DNS). This is better than the default bridge because it offers service discovery.

Top comments (0)