CoursesDocker in depthThe container network model & drivers

The container network model & drivers

CNM, bridge, host, macvlan, overlay.

Intermediate14 min · lesson 11 of 30

Every container comes up like a fresh laptop on a desk. It has a network card, it has a routing table, and nothing is plugged into the port yet. Docker's name for the wiring scheme is the Container Network Model (CNM), and three pieces of it are worth naming. The sandbox is the container's own private network stack: its interfaces, its routes, and its Domain Name System (DNS) settings, DNS being the service that turns a name like db into an address. All of that is walled off from the host. The endpoint is the cable. The network is the switch that cable plugs into. A driver decides what kind of switch you get, and both reachability and isolation fall straight out of that one choice. Learn the drivers and you have learned the model.

Five drivers are worth knowing. bridge, host, and none all live inside a single machine. overlay and macvlan exist for the jobs one machine cannot cover on its own. Most days you will create a bridge network and forget the rest are there. The other four each answer one narrow question, and the skill is noticing which question you are actually being asked before you reach for an answer.

Which driver?
Pick a network driver
Reachability and isolation both come from this one choice.
the default
bridge
One host. Containers talk by name and share the host's address going out. The right answer most days.
max speed
host
Share the host's network stack. No published ports, no isolation, lowest latency.
no network
none
Loopback only. For jobs that must never reach out.
across hosts
overlay
One virtual network spanning nodes. Swarm services live here.
on the office network
macvlan
Its own address on the physical network. Looks like a separate machine.
Start at bridge. Move off it only when a requirement forces you to.

bridge builds a private virtual network on the host and hands out addresses from a subnet you pick. Traffic heading outward gets Network Address Translation (NAT), the same trick your home router plays on your phone and your TV, so everything shares one address on the way out and you publish ports to let the replies find their way back in. host throws the sandbox away and binds the container straight onto the host's own interfaces. Latency is as low as it gets. There is also no separate address, no isolation, and no -p publishing, because the container's ports are the host's ports. none gives a container a loopback interface and nothing else, which is exactly right for a batch job that grinds through local files and should never phone home. overlay stitches several machines into one flat network, so a Swarm service on node A can reach a service on node B by name. macvlan is the odd one out. It hands the container its own Media Access Control (MAC) address, the hardware-level name a network card answers to, plus its own Internet Protocol (IP) address on the physical local area network (LAN), the wired network your office or lab already runs. To everything else on that wire, the container looks like a separate machine.

Build your own bridge, then look inside it

Leave the default bridge network alone. It survives for backward compatibility and it ships without name resolution on purpose, so two containers sitting on it can only find each other by address, and addresses move. A network you create yourself fixes that. Docker runs a small DNS resolver at 127.0.0.11 for every container on the network, and each one can reach the others by container name. One command sets it up.

terminal
$ docker network create --driver bridge --subnet 172.30.0.0/24 appnet
c4e5a1b8f7d3902e6a1b4c9d0e2f3a5b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
c4e5a1b8f7d3 appnet bridge local
1a2b3c4d5e6f bridge bridge local
5f9e8d7c6b4a host host local
0a1b2c3d4e5f none null local

docker network inspect is where the model stops being an idea and becomes a record you can read. Attach a container, then ask Docker what it wrote down: the subnet, the gateway, and every connected container with the address it was handed.

terminal
$ docker run -d --name api --network appnet nginx:1.27
7a2c9f4b1e8d5c3a0f6b9d2e4c7a1b8f0e3d6c9a2b5f8e1d4c7a0b3f6e9d2c5a
$ docker network inspect appnet
[
{
"Name": "appnet",
"Id": "c4e5a1b8f7d3902e6a1b4c9d0e2f3a5b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Config": [
{ "Subnet": "172.30.0.0/24", "Gateway": "172.30.0.1" }
]
},
"Containers": {
"7a2c9f4b1e8d5c3a0f6b9d2e4c7a1b8f0e3d6c9a2b5f8e1d4c7a0b3f6e9d2c5a": {
"Name": "api",
"MacAddress": "02:42:ac:1e:00:02",
"IPv4Address": "172.30.0.2/24",
"IPv6Address": ""
}
},
"Options": {},
"Labels": {}
}
]

Split the tiers across two networks

Here is a shape you will meet over and over. The web tier has to be reachable from outside. The database has to be reachable from web and from nowhere else. Nothing else on the box should be able to open a socket to it at all. Networks give you that without writing a firewall rule. A container can talk only to the networks it is attached to, so park the database alone on a backend network, put web on a frontend network for inbound traffic, then attach web to backend as well. web can now see the database. Anything else living on frontend cannot.

terminal
$ docker network create frontend
5b1c8a2f9d4e7c0a3b6f1d8e5c2a9b4f7e0d3c6a1b8f5e2d9c4a7b0f3e6d1c8a
$ docker network create backend
8d3e0c7a4b1f6e9d2c5a8b0f3e6d1c4a7b2f5e8d0c3a6b9f1e4d7c0a3b6f9e2d
$ docker run -d --name db --network backend postgres:16
a1f2b3c4d5e6f7089a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f6071
$ docker run -d --name web --network frontend -p 8080:3000 myapp:1.0
b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f7081920314253647586970a
$ docker network connect backend web

Drawings lie. Go and test the boundary. A throwaway busybox container on backend should resolve db by name, and the same throwaway on frontend should come back empty-handed.

terminal
$ docker run --rm --network backend busybox nslookup db
Server: 127.0.0.11
Address: 127.0.0.11:53
Name: db
Address: 172.19.0.2
$ docker run --rm --network frontend busybox nslookup db
Server: 127.0.0.11
Address: 127.0.0.11:53
*** Can't find db: No answer

When a container needs a real address on the LAN

Sometimes a bridge will not do. A legacy service expects to sit on the office subnet with an address of its own, or a monitoring agent has to send from an address the network team already trusts and has no appetite to re-approve. macvlan attaches the container to a physical interface and gives it a genuine address on that subnet. No NAT. No published ports. It turns up on the LAN the way any other box does.

terminal
$ docker network create -d macvlan \
--subnet=192.168.1.0/24 --gateway=192.168.1.1 \
-o parent=eth0 lan
3b7e0d9c2a5f8e1d4c7a0b3f6e9d2c5a8b1f4e7d0c3a6b9f2e5d8c1a4b7f0e3d
$ docker run -d --network lan --ip 192.168.1.50 --name probe nginx:1.27
9f2a5c8e1b4d7a0f3c6b9e2d5a8f1c4b7e0d3a6c9b2f5e8d1a4c7b0f3e6d9a2c

From another machine on the same LAN, and this part matters, not from the Docker host itself, the container answers on its own address like any other box on the subnet.

terminal
$ curl -s -o /dev/null -w '%{http_code}\n' http://192.168.1.50
200
macvlan hides the container from its own host
A macvlan parent interface is not allowed to talk to the sub-interfaces hanging off it. That is the design, not a bug. So a health check, a reverse proxy, or a metrics scrape running on the Docker host will time out against probe while every other machine on the LAN reaches it without trouble. The way out is to add a macvlan shim interface on the host and route to the container subnet through that. Two more traps before you ship this. Most cloud provider network interface cards (NICs), the virtual network hardware your instance is given, block the promiscuous mode macvlan depends on, so this will not work on EC2 or GCE at all. And every macvlan container eats a real address on your subnet, so a busy host can drain a /24 far faster than you expect.

The driver you pick is the isolation you get

CNM hands you sandboxes, endpoints, and networks. The driver decides how they behave. bridge is the single-host default with NAT on the way out. host deletes network isolation. none is total silence. macvlan puts containers on the LAN with real MAC addresses. overlay spans Swarm nodes. Choose for the topology and the threat model in front of you, not for the flag your fingers already know.

Networks you create get the embedded resolver; the legacy default bridge does not behave the same way for service discovery, so reach for docker network create in anything you plan to keep. host networking buys you raw throughput and one less layer to reason about, and it costs you isolation and any clarity about which port belongs to whom. On a shared host, treat host mode as a privilege somebody granted, and write down who granted it.

When two Compose projects land on the same subnet, you get silent overlap. Set default address pools in daemon.json, or pin explicit subnets in the Compose files, and the collision cannot happen. Overlapping routes rarely announce themselves. They surface as timeouts that look random until someone reads the routing table and finds traffic leaving by one path and trying to come back by another.

Before you change networking on a machine that matters, capture docker network ls and a docker network inspect of the networks involved, and note which host you ran them on. Record the attachment you added, which container onto which network, so the one link to undo is written down rather than remembered. That is the difference between a two-minute rollback and an hour spent guessing which of three networks a container picked up its address from. If a teammate cannot replay the change from the ticket alone, including the subnet you chose and the nslookup answers you saw on a healthy system, the runbook is not finished.

Try this

Run this on a lab engine; Docker 24 or newer is fine. Read the sample output first, so you know what a healthy result looks like before you lean on the command somewhere it counts.

terminal
$ docker network create --driver bridge appnet
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
c0ffe… appnet bridge local
$ docker network inspect appnet --format '{{.Driver}} {{json .IPAM.Config}}'
bridge [{"Subnet":"172.18.0.0/16","Gateway":"172.18.0.1"}]
# STATUS: user-defined bridge ready for DNS-based discovery

Takeaway

Name the three pieces, sandbox, endpoint, and network, then pick the driver on purpose. bridge for anything that lives on one host. host, none, macvlan, or overlay only when a written requirement drags you there. Create your own networks so names resolve, and treat host mode as raised privilege every single time.

Quick check
01A busybox container is attached only to the user-defined bridge frontend. It runs nslookup db, and db sits only on backend. What comes back?
Incorrect — The resolver at 127.0.0.11 only answers for containers that share a network with whoever is asking. Names on other networks are invisible to it.
Correct — Names are scoped per network. Attach the container to backend as well, or let Compose do it for you, if it needs to reach db.
Incorrect — Networks you create are isolated from one another, so there is no route to refuse in the first place. The lookup fails before any connection is attempted.
Incorrect — Nothing bridges two user-defined networks on its own. You run docker network connect yourself.
02How does the host network driver differ from a bridge network?
Incorrect — That describes macvlan, which puts the container straight onto the physical LAN with hardware and network addresses of its own.
Incorrect — Spanning several hosts is the overlay driver's job.
Incorrect — Loopback and nothing else describes the none driver.
Correct — host mode throws the sandbox away, so the container's ports are literally the host's, and there is nothing left to publish or isolate.
03You put a service on a macvlan network with its own LAN address. Other machines on the LAN reach it fine, but a health check running on the Docker host times out. Why?
Incorrect — macvlan does not use -p at all. What blocks the host is the parent and sub-interface rule, not a missing publish.
Correct — The parent interface is cut off from its own macvlan children by design, and a shim gives the host a path back to them.
Incorrect — The block is the macvlan parent and child rule, not a missing route or a NAT gap. Every other machine on the LAN gets through fine.
Incorrect — That is a real cloud trap, but a separate one. Here the other machines reach the container, so macvlan itself is working.

Related