Networks, drivers and DNS
Bridges, veth pairs, the embedded DNS server, internal networks, ipvlan and legacy links.
You'll work on the main lab VM (secopslog-docker) here. 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.9.2 daemon. The table lists the three networks every install creates. bridge is the default bridge, the one a container joins when you pass no --network. The other two are host and none (driver null). "Container networking basics" (Docker for beginners) showed the everyday difference between the default bridge and a network you create. Now you'll open 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 and read back what Docker recorded. The subnet is fixed so the addresses below stay the same between runs; without --subnet Docker picks one from its address pools.
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 isn't a container. It's an address Docker puts on a Linux bridge interface in the host. Look for it, and for what's 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. One interface is attached to it with master br-...: veth948054f, interface number 357 on this host. A veth is a virtual cable with two ends. The @if2 says the other end is interface 2 in another network namespace. Here's the container's side:
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. It also works on images without any tools, which "Network troubleshooting" relies on. Inside, you'll see a loopback interface and eth0@if357: the other end of the cable, pointing back at host interface 357. The container's default route goes to the bridge address. Interface numbers, veth names and the bridge name change with every container and network, but the pairing rule doesn't. "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. Here that's 192.168.2.1, this Multipass VM's gateway; yours will be whatever your host uses. Nothing in that file knows container names. That's why containers on the default bridge can't find each other by name. On a user-defined network the only nameserver is 127.0.0.11. The comment shows where other 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. You'll find the answer in the container's own NAT table:
Inside each container's namespace, Docker adds rules that send port 53 on 127.0.0.11 to two random ports. The process listening there is dockerd: the daemon opened sockets inside the container's namespace. The ports differ per container. Two consequences follow. First, name resolution between containers depends on a running dockerd. With live-restore on, containers keep running while the daemon is down, but they can't resolve each other's names until it's back. Second, 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 use the first. So this spreads load roughly at best. It also knows nothing about health: a stopped container drops out of the answers, a hung one doesn't. Clients also cache. The JVM keeps good lookups for networkaddress.cache.ttl. Under a security manager that can be forever. After a container is recreated with a new address, your 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 isn't on any network lab-web belongs to. So the embedded server doesn't know the name and passes it upstream, which can't resolve it either. You get SERVFAIL here, and 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. That's 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 can't reach anything beyond lab-back. That's an internal network: Docker sets up no gateway route and no masquerading for it. lab-web still reaches the internet through lab-front. Which tiers get egress, and how you control it, is covered in "Container network hardening" (Advanced container security).
none and host
With --network none the namespace has loopback and nothing else. With --network host there's 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, and -p is ignored. Nothing filters its traffic separately from the host's, so treat it as a privilege, as "Host namespace sharing as attack surface" (Advanced container security) does.
ipvlan and macvlan
Both drivers hang containers off a parent interface, not a bridge. Containers get addresses on the parent's network, with no NAT and no published ports. Without -o parent=, Docker creates a dummy parent. That's 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's ipvlan's defining property. Every container shares the parent's MAC and is told apart by IP address. That 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. You should know three things before you choose it. The host can't 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 didn't assign, so macvlan often fails there. ipvlan avoids that. And 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's 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 into the client as OLD_ENV_* variables. Passwords came along too. 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:
Try this
Before you run the clean-up step, run docker stop lab-api2 and repeat the nslookup -type=a api query from a throwaway container on lab-front. Predict how many records you'll get, then check. Then run the same query on --network lab-back and compare: an alias only exists on the network it was given on, so you should expect a failed lookup there.
Takeaway
Name resolution follows network membership, so a lookup that fails with SERVFAIL usually means the two containers don't share a network, not that the target is down. Keep in mind that 127.0.0.11 is answered by dockerd through NAT rules inside each container. A firewall script that flushes that table, or a daemon that's down, breaks names while every container still looks healthy.
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?