Container Networking Deep Dive Part 3: Docker Network Drivers Compared
Part 2 covered the bridge driver in depth: docker0, veth pairs, NAT, embedded DNS. Bridge is the default because it is the right choice most of the time, but Docker ships four other network drivers, and each one exists because bridge’s tradeoffs are wrong for a specific case. This post goes through each, not as a feature checklist, but as what you give up and what you get.
bridge: isolation by default, NAT by necessity
Covered fully in Part 2. The tradeoff in one sentence: every container gets its own namespace and IP, reachable from the host and other containers on the same bridge, reachable from outside only through NAT and explicit port publishing. This is correct for the overwhelming majority of workloads, a web server, a database, a batch job, where you want isolation and don’t need the container to be a first-class citizen on the physical network.
The cost is exactly what NAT always costs: the container’s real IP is never visible outside the host, connection tracking has to keep state for every flow, and anything that needs to be seen at its own address on the LAN (an appliance-style service, something that needs its own DHCP lease, some legacy protocol that doesn’t tolerate NAT) doesn’t fit this model.
host: no isolation, no NAT, no cost
docker run -d --name app-host --network host nginx
There is no veth pair, no bridge, no namespace boundary for networking at all. The container shares the host’s network namespace directly. ip addr inside the container returns the host’s real interfaces, and a process listening on port 80 inside the container is listening on port 80 of the host, with no -p flag needed or possible.
The tradeoff: zero NAT overhead, zero veth-pair packet-copy cost, and the container can bind to any host port directly, genuinely useful for a small number of latency-sensitive workloads or ports too numerous to publish individually. The cost is that there is no port isolation between containers at all. Two containers both using --network host and both trying to bind port 8080 conflict exactly like two ordinary processes would, because that is precisely what they are from the network stack’s point of view. There is also no network-layer isolation between the container and the host. A compromised container on host networking can see and interact with every socket the host itself has open.
macvlan: the container gets its own MAC on the physical network
docker network create -d macvlan \
--subnet 192.168.1.0/24 \
--gateway 192.168.1.1 \
-o parent=eth0 macvlan-net
docker run -d --name app-macvlan --network macvlan-net \
--ip 192.168.1.50 nginx
A macvlan network gives the container its own MAC address, carved directly off the parent interface (eth0 above), and its own IP on the physical LAN’s subnet, not a private Docker-only range. From the network’s perspective, the container is not behind NAT at all. It is a genuine additional host on 192.168.1.0/24, with its own ARP entry, discoverable by anything else on that LAN exactly as if it were a separate physical machine.
ip -d link show eth0 | grep macvlan
The parent interface shows multiple macvlan sub-interfaces bound to it, one per container, each with a distinct MAC. This is the driver that answers the requirement “this container needs its own real IP on the office LAN, not a NAT’d address”: legacy appliances, anything using MAC-based licensing or ACLs, or migrating a bare-metal service into a container without changing how the rest of the network talks to it.
The costs are specific. Most cloud provider virtual NICs (and many hypervisor vNICs) block or heavily restrict multiple MAC addresses per interface for security reasons, so macvlan frequently does not work at all in cloud VMs without provider-specific configuration. This is a bare-metal and traditional-hypervisor pattern more than a cloud-native one. And by default the host itself cannot talk to a macvlan container directly, because the kernel’s macvlan implementation doesn’t route between the parent interface and its own macvlan children. You need a macvlan sub-interface on the host too, added deliberately, to bridge that specific gap.
ipvlan: same idea, one shared MAC
docker network create -d ipvlan \
--subnet 192.168.1.0/24 \
--gateway 192.168.1.1 \
-o parent=eth0 -o ipvlan_mode=l2 ipvlan-net
ipvlan solves the same problem as macvlan, a real LAN-routable IP per container instead of NAT, but every container shares the parent interface’s single MAC address, with the kernel distinguishing traffic by IP instead. This matters specifically where the upstream switch enforces a MAC-address-count limit per port (common on managed switches with port security enabled) that would silently drop traffic from extra macvlan MACs. ipvlan trades away per-container MAC identity, no MAC-based ACLs, no MAC-based DHCP reservations per container, in exchange for working behind switch ports that macvlan can’t.
none: no networking at all
docker run -d --name app-isolated --network none alpine sleep infinity
docker exec app-isolated ip addr show
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
Only loopback. No veth pair is created at all. This is the correct driver for anything that genuinely has no network requirement: a batch processing job reading and writing only local volumes, a build step, anything where the absence of network access is itself the security property you want. It is also the starting point for building a fully custom network setup by hand, attaching interfaces after the container starts, for cases none of the other four drivers fit.
Choosing one
| Driver | Isolation | Real LAN address | Typical use |
|---|---|---|---|
| bridge | Full (NAT’d) | No | Default, most services |
| host | None | N/A (host’s own) | Latency-sensitive, port-heavy workloads |
| macvlan | Full (own MAC+IP) | Yes | Legacy appliances, bare-metal migration, MAC-based ACLs |
| ipvlan | Full (shared MAC) | Yes | Same as macvlan, behind switches with MAC-count limits |
| none | Total | N/A | No network requirement, or fully custom setup |
The pattern across all five: nothing here is a Docker-specific concept. bridge is a Linux bridge plus NAT. host is “don’t create a namespace.” macvlan and ipvlan are kernel driver features that predate container runtimes by years, applied to a container’s namespace instead of a VM’s. none is simply skipping the veth pair. Docker’s contribution in every case is orchestrating the underlying primitive consistently and cleaning it up on docker stop, the same theme as Part 2’s iptables rules.
Everything so far has stayed on one host. The next post crosses the wire: Docker Swarm’s overlay network driver, the VXLAN encapsulation underneath it, and what that encapsulation does to the MTU math on every packet that crosses it.