Container networking basics
Published ports, user-defined networks and name resolution.
ping: bad address 'lab-web' is the first networking error most people meet. The container lab-web is running. The second container simply can't turn that name into an address, because both were started without --network. You'll see why, then build the setup that works. Containers sit on a network you created and reach each other by name. Only the services that need outside traffic are published on the host. Everything runs on the main lab VM (secopslog-docker), and there are no lesson files. Work in a new directory, mkdir -p ~/lab/networking && cd ~/lab/networking.
The default bridge has addresses, not names
Docker creates three networks when it is installed:
bridge is a private network inside the host, behind a Linux bridge interface called docker0. Every container started without --network joins it. host removes network isolation and puts the container directly on the host's network stack. none gives the container only a loopback interface. The network IDs differ on every machine.
Start Nginx on the default bridge and try to reach it by name:
The name fails and the IP address works. The default bridge doesn't register container names in any DNS, so lab-web means nothing to the second container, while 172.17.0.2 is reachable. The {{range .NetworkSettings.Networks}} template reads the address from the container's per-network settings. Docker 29 removed the old top-level .NetworkSettings.IPAddress field, so templates in older guides fail. The address is handed out when the container starts and can be different after a restart. That's why an IP address in a config file is a bug waiting to happen.
A user-defined network gives you names
Create a bridge network of your own and put the web server on it:
Containers attached to a user-defined network get Docker's embedded DNS server, which answers at 127.0.0.11 inside every container on that network. nslookup shows it as the Server, and the answer maps lab-web to its current address. The output has two Non-authoritative answer: headers. nslookup asked for an IPv4 (A) record and an IPv6 (AAAA) record, and the network has no IPv6. The subnet comes from Docker's default address pools, so you may see 172.18 or another 172.x range. The wget by name proves the whole path, name to address to port 80.
The container's resolver configuration shows how this is wired:
Docker generated /etc/resolv.conf with nameserver 127.0.0.11. Names of containers on the network are answered locally. Anything else (a package mirror, a public API) is forwarded to the host's resolver, listed as ExtServers. docker network inspect shows which containers are attached. That's the first thing you should check when a name doesn't resolve:
A container can also answer to extra names on a network with --network-alias. Compose uses exactly this mechanism to make each service name resolvable, as the next lesson shows. Drivers, aliases and the DNS server in more depth are in "Networks, drivers and DNS" in Docker in depth.
Publishing decides who can reach a port
Traffic between containers on lab-net needs no -p. Publishing is for traffic that starts outside: a browser, another machine, a tool on the host. Publish one container on all addresses and one on loopback only:
-p 8080:80 was published twice, on 0.0.0.0 (every IPv4 address of the host) and [::] (every IPv6 address). -p 127.0.0.1:8081:80 exists only on the loopback address. Now test both from the VM's own LAN address, the way another machine would see them:
Port 8080 answered 200 on the LAN address. Port 8081 didn't answer at all on that address (curl exit status 7, could not connect) but answered on 127.0.0.1. On a laptop or a server, a database or admin UI that only you need belongs on 127.0.0.1, or not published at all. Your 192.168.2.6 will be a different address.
The daemon writes firewall rules for every published port, through a firewall backend that docker info reports:
Reachable by name, still refused
A name that resolves is half the job. The server inside the container also has to listen on an address other containers can reach. A small Node server bound to 127.0.0.1:
From inside its own container the server answers. From a neighbour on the same network, the name resolves (the error shows the address), but the connection is refused. Each container has its own network stack and its own loopback interface, so 127.0.0.1 inside lab-loop is reachable only from inside lab-loop. Development servers that default to localhost (many do) cause this. Bind to 0.0.0.0 instead:
When two containers can't talk, check three things in order. Are both attached to the same network (docker network inspect)? Does the name resolve (nslookup from a container on that network)? Is the server listening on 0.0.0.0 on the port you're calling? Publishing (-p) isn't on that list.
Host networking and the rest
With --network host the container uses the host's interfaces directly. Its ports are already the host's ports, so Docker discards -p with a warning. Host networking avoids NAT, at the price of isolation and of port clashes with everything else on the host. Use it only when you have measured a reason. --network none leaves only loopback, for batch jobs that should have no network at all.
One problem appears only on some machines: the container can't reach a company host while you're connected to a VPN. The usual cause is an address overlap. Docker takes subnets from its default pools: 172.17.0.0/16 for the default bridge, then further 172.x and 192.168.x ranges for user-defined networks. If a VPN or office LAN routes the same range, traffic meant for it stays inside Docker. The fix is to give Docker different pools (default-address-pools), covered in "Configuring the daemon safely" in Docker in depth.
Clean up the lab:
Try this
While lab-web is still on lab-net, run docker run --rm alpine:3.24 nslookup lab-web once without --network and once with --network lab-net. Predict which lookup finds the name, using the default-bridge ping failure above as your hint, then compare the Server lines of the two answers.
Remember
Put containers that talk to each other on a network you create, call them by name, and bind servers inside to 0.0.0.0. The two failures here look alike but aren't. bad address 'lab-web' on the default bridge is a name problem, while lab-loop resolved to 172.18.0.5 and was still refused because it listened on 127.0.0.1. Adding -p fixes neither.
web and api were both started with plain docker run -d and no network option. Inside web, the config says API_URL=http://api:8000, and the logs show bad address 'api'. What fixes it without hard-coding an IP?wget http://lab-loop:9000 prints can't connect to remote host (172.18.0.5): Connection refused, while docker exec lab-loop wget -qO- http://127.0.0.1:9000 works. What is wrong?bad address; here the name resolved to 172.18.0.5.