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 cannot turn that name into an address, because both were started without --network. This lesson shows why, then builds the setup that works: containers on a network you created, reaching each other by name, with only the services that need outside traffic 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 does not 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, which is 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 because 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, which is the first thing to check when a name does not 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 did not 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.4 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) and 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 cannot talk, check in this order: are both attached to the same network (docker network inspect), does the name resolve (nslookup from a container on that network), and is the server listening on 0.0.0.0 on the port you are calling. Publishing (-p) is not on that list.
Host networking and the rest
With --network host the container uses the host's interfaces directly, so its ports are already the host's ports and 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 cannot reach a company host while 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), and 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:
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.Try this
Work through “Host networking and the rest” 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 container networking basics, keep “Host networking and the rest”. Decide now which check you will run when this shows up on a live system, and write it somewhere your team will find it.