Networks, drivers and DNS
Bridges, veth pairs, the embedded DNS server, internal networks, ipvlan and legacy links.
Use the main lab VM (secopslog-docker) for this lesson. Two settings decide how a container is networked: the driver of each network it joins, and which networks those are. Start with what this daemon can build:
The first line lists the network drivers compiled into this Docker 29.8.2 daemon. The table lists the three networks every install creates: bridge (the default bridge, the one a container joins when you pass no --network), host and none (driver null). "Container networking basics" (Docker for beginners) showed the everyday difference between the default bridge and a network you create. This lesson opens those networks up: what the kernel objects are, how name resolution is wired in, and what the other drivers give you.
What a bridge network is made of
Create a user-defined bridge network with a fixed subnet (fixed so the addresses below stay the same between runs; without --subnet Docker picks one from its address pools) and read back what Docker recorded:
ipv4=true ipv6=false is the default: IPv6 is opt-in per network, which matters in "Publishing ports and the packet path". internal=false means the network has a route to the outside. The gateway 10.89.1.1 is not a container; it is an address Docker puts on a Linux bridge interface in the host. Look for it, and for what is plugged into it:
Docker names the bridge br- plus the first 12 characters of the network ID, so your name will differ. The gateway address sits on the bridge, and one interface is attached to it with master br-...: vethf4537c0, interface number 1209 on this host. A veth is a virtual cable with two ends, and the @if2 says the other end is interface 2 in another network namespace. The container's side looks like this:
nsenter -n -t PID runs a host command inside the network namespace of process PID, here the container's main process; it needs root, hence sudo, and works on images without any tools, which "Network troubleshooting" relies on. Inside, there is a loopback interface and eth0@if1209: the other end of the cable, pointing back at host interface 1209. The container's default route goes to the bridge address. Interface numbers, veth names and the bridge name change with every container and network; the pairing rule does not. "Namespaces from first principles" (Advanced container security) covers network namespaces next to the other seven kinds.
The embedded DNS server
Compare what a container sees as its resolver on the default bridge and on lab-front:
On the default bridge, Docker copies the host's upstream resolvers into the container (192.168.2.1 is this Multipass VM's gateway; yours will be whatever your host uses). Nothing in that file knows container names, which is why the default bridge has no name resolution between containers. On a user-defined network the only nameserver is 127.0.0.11, and the comment shows where unknown names go: ExtServers: [host(127.0.0.53)], the host's systemd-resolved, queried from the host's namespace. --dns on docker run changes those upstream servers, not the 127.0.0.11 line. Docker regenerates the file until you edit it inside the container, then leaves your edit alone, as the header says.
127.0.0.11 is a loopback address inside the container, yet no process in the container answers it. The answer is in the container's own NAT table:
Docker installs rules inside each container's namespace that redirect port 53 on 127.0.0.11 to two random ports, and the process listening there is dockerd: the daemon opened sockets inside the container's namespace. The ports differ per container. Two consequences follow. Name resolution between containers depends on a running dockerd; with live-restore on, containers keep running while the daemon is down, but they cannot resolve each other's names until it is back. And a container with its own firewall tooling must not flush its NAT table, or DNS stops.
The server answers for container names, for the service names Compose registers, and for aliases. An alias is an extra name on one network; give the same alias to two containers and the server returns both addresses:
Both containers answer to api. The order of the records can change between queries, and most clients connect to the first, so this spreads load roughly at best and knows nothing about health: a stopped container drops out of the answers, a hung one does not. Clients also cache. The JVM keeps successful lookups for networkaddress.cache.ttl, which can be forever under a security manager, so after a container is recreated with a new address the application may keep dialling the old one until it reconnects or restarts. Swarm services use a virtual IP by default and DNS round robin only with endpoint_mode: dnsrr; "Overlay networks and the routing mesh" covers both.
Segmenting with networks
A container can reach and resolve only containers on networks it shares. Put a cache on a second network, created with --internal, and ask for it from lab-web:
lab-cache is not on any network lab-web belongs to, so the embedded server does not know the name and passes it upstream, which cannot resolve it either: SERVFAIL here, NXDOMAIN with other upstream resolvers. Either way the cause is network membership, not the cache. Connect lab-web to the second network and the name and the port work:
docker network connect added a second interface to the running container, and it now has one address per network. This is the usual two-tier shape: the web tier on a front network, the data tier on a back network that only the web tier joins. --internal on lab-back changes one more thing:
The cache has a route for its own subnet and no default route, and its DNS lookups for outside names fail, so it cannot reach anything beyond lab-back. That is an internal network: Docker sets up no gateway route and no masquerading for it. lab-web still reaches the internet through lab-front. Choosing which tiers get egress, and controlling it, is the subject of "Container network hardening" (Advanced container security).
none and host
With --network none the namespace has loopback and nothing else. With --network host there is no separate namespace at all: /proc/self/ns/net in the container names the same namespace (4026531833) as PID 1 on the host, while an ordinary container gets its own. A host-network container binds host ports directly, -p is ignored, and nothing filters its traffic separately from the host's; treat it as a privilege, as "Host namespace sharing as attack surface" (Advanced container security) does.
ipvlan and macvlan
Both drivers attach containers to a parent interface instead of a bridge, so containers get addresses on the parent's network, with no NAT and no published ports. Without -o parent=, Docker creates a dummy parent, which is enough to see how ipvlan works without touching a real LAN:
Both containers have the same MAC address, the MAC of the dummy interface di-... that Docker created as the parent. That is ipvlan's defining property: every container shares the parent's MAC and is told apart by IP address, which suits switch ports and cloud networks that accept only one MAC per port. Containers on the same ipvlan network reach each other, here by name. With a real parent (-o parent=eth0, plus the LAN's --subnet and --gateway) they become reachable from the LAN; -o ipvlan_mode=l3 makes the host route between container subnets instead of bridging at layer 2. A network with a dummy parent is internal by design.
macvlan gives every container its own MAC address on the parent's network, so to the LAN each container looks like a separate machine. Three things to know before choosing it. The host cannot reach its own macvlan containers through the parent interface; the usual fix is a macvlan sub-interface on the host with a route to the container addresses. Many cloud and virtualised networks drop frames from MAC addresses they did not assign, so macvlan often fails there; ipvlan avoids that. Each container takes a real address from the LAN. Since Docker 29.0, macvlan and ipvlan L2 networks get no default gateway at all, IPv4 or IPv6, unless the IPAM configuration includes --gateway; give one explicitly when containers must route beyond the LAN. macvlan is described here and not run: the lab VM sits behind its hypervisor's NAT, so there is no LAN on which to show the result.
Legacy links
Old scripts and Compose files still use --link and links:. On Docker 29 a link on the default bridge does this:
Creating the container prints a deprecation warning (Docker 29.6 and later), the link writes one /etc/hosts entry with the target's address at start time, and no OLD_* environment variables appear. Before Docker 29.0 a link also copied the target's environment, passwords included, into the client as OLD_ENV_* variables; that copy is off by default in 29.x (DOCKER_KEEP_DEPRECATED_LEGACY_LINKS_ENV_VARS=1 in dockerd's environment restores it until removal in 30.0), so on older hosts treat existing links as a secret leak. Links on the default bridge are scheduled for removal in 30.0; on a user-defined network a link still works and acts as an alias. To migrate, put both containers on a user-defined network and use the container name or a --network-alias.
Rootless Docker builds its networks differently (the daemon runs in its own user and network namespace and reaches the outside through a userspace network stack); "Rootless Docker" (Advanced container security) covers what changes. Clean up:
app resolves other containers by name. A teammate runs a new container with no --network flag and it cannot resolve them, although it reaches the internet. What explains both observations?--internal network and the application container on that network plus a normal bridge network. What can the database container do?docker exec app nslookup db worked until a firewall script inside app flushed the container's NAT table. Now nothing in app resolves db, although both containers are running and still share their network. Why?Try this
Work through “Legacy links” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.
Takeaway
If you keep one thing from networks, drivers and dns, keep “Legacy links”. Decide now which check you will run when this shows up on a live system, and write it somewhere your team will find it.