Container networking basics
bridge, host, none, and publishing ports.
Two containers on the same laptop can still fail to reach each other. Each container gets a private IP when it starts. Restart it and the IP usually changes, so hard-coding that address in config breaks. Put the containers on a user-defined network and use the container --name as the hostname instead.
Every container joins a network the second it starts. Docker builds three for you before you ask for anything: bridge, host, and none. Each one runs on a different driver, which is Docker's word for the style of networking on offer. A bridge network is a small private network living on your host (the machine Docker itself runs on), fenced off from the network your computer uses for email and browsing. It is also the default, so it is what you will reach for nearly every time. A host network takes the fence away and lets the container share your machine's network directly. You gain a little speed in a few narrow situations, and you give up the isolation the bridge was handing you. The -p publishing you will meet later stops working too. A none network gives the container no networking whatsoever, for work that should stay sealed shut. The rest of this lesson stays on bridge, because that is the one you will use over and over.
Names go nowhere on the default bridge
Start a web server, then try to reach it by name from a throwaway second container. Neither command names a network, so both containers land on Docker's default bridge.
$ docker run -d --name web nginx:alpine$ docker run --rm busybox ping -c 1 web
d9b1c0f7a4e83b2c6f1a0d5e9c8b7a6f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9bping: bad address 'web'
Read the second line slowly. busybox could not resolve the name web, so it sent nothing. The web container is up. The default bridge network has no DNS for container names. Containers there can still use raw IPs; names are not registered.
Give your containers a network of their own
Create your own bridge network and Docker starts DNS for that network. Each attached container is resolvable by --name. You do not configure a DNS server. Repeat the same test with both containers on that network.
$ docker rm -f web$ docker network create appnet$ docker run -d --name web --network appnet nginx:alpine$ docker run --rm --network appnet busybox ping -c 1 web
weba3f21c9d7b4e6082f5a1c0d9e83b746024c1f8a9d0e73b645c2b1a09f8e7d6c3b81f4c0a9e2d7635c8a0f4e1d2b709536f1a8c4e07d3b9625a1c8e40f7b0d3a6PING web (172.18.0.2): 56 data bytes64 bytes from 172.18.0.2: seq=0 ttl=64 time=0.087 ms--- web ping statistics ---1 packets transmitted, 1 packets received, 0% packet lossround-trip min/avg/max = 0.087/0.087/0.087 ms
The name web now resolves to 172.18.0.2 and the ping lands. A ping only proves the lookup worked, though. Real apps talk over real ports, so pull down the web server's home page by name and test the whole path from one end to the other.
$ docker run --rm --network appnet busybox wget -qO- http://web
<!DOCTYPE html><html><head><title>Welcome to nginx!</title><style>html { color-scheme: light dark; }body { width: 35em; margin: 0 auto;font-family: Tahoma, Verdana, Arial, sans-serif; }</style></head><body><h1>Welcome to nginx!</h1><p>If you see this page, the nginx web server is successfully installed andworking. Further configuration is required.</p><p>For online documentation and support please refer to<a href="http://nginx.org/">nginx.org</a>.<br/>Commercial support is available at<a href="http://nginx.com/">nginx.com</a>.</p><p><em>Thank you for using nginx.</em></p></body></html>
That is how a real stack wires itself together. A web container talks to a database at the name db, or to a cache (a small store that hands back data fast) at the name redis, and Docker's built-in DNS turns each name into whatever IP that container is holding at this moment. No addresses in your config. No restarts to chase.
Publishing a port opens the front door
Everything so far has stayed inside appnet. The bridge is private, so nothing on another machine can load that web container, and on Docker Desktop, where Docker runs inside a small virtual machine, your own browser cannot either. To let outside traffic in, you publish a port with -p. A port is a numbered door on a machine with one program waiting behind it, and the format reads -p HOST:CONTAINER. It runs a wire from a door on your machine through to a door inside the container.
$ docker run -d --name web-public -p 8080:80 --network appnet nginx:alpine$ curl -s http://localhost:8080 | head -n 4
5f2ad8c1b9e04a7c3d6f0b8e2a9c1d4f7b0e3a6c9d2f5b8e1a4c7d0f3b6e9a2c<!DOCTYPE html><html><head><title>Welcome to nginx!</title>
Hold the two paths apart in your head. Containers on the same network reach each other by name with no -p anywhere in the command, and that traffic never leaves Docker's private network. Publishing is for the front door only, for traffic arriving from outside the network Docker built, which includes the browser on your own machine. A database that nothing but your app talks to should stay unpublished, sealed on the internal network where no stranger can knock. The public-facing web server is the one that needs -p.
One detail is worth pinning down before you move on. A container on the default bridge can still reach the outside world, so installing packages or curling a public API from it works fine. What it does not get is a dependable name for the container next door. A network you create yourself closes that gap, and every container you attach can find the others by container name. That is the machinery Docker Compose (the tool that starts a whole stack of containers from one file) sits on top of later, when it builds a network for your stack and registers each service under its service name.
-p publishes a container port on the host, which is how traffic from outside Docker reaches the process. Default -p 8080:80 binds every host interface, so anything that can reach the machine can hit that port. -p 127.0.0.1:8080:80 limits it to localhost. Docker's published-port rules are applied in the kernel before typical host firewall tools, so a "closed" ufw/firewalld policy often does not cover -p. Publishing every container "to be safe" is how a database ends up on the LAN for no reason. Containers on a user-defined network talk on container ports without -p.
Three checks when they cannot connect
Work through these in order. Are they on the same network? Are you using the right container name and the right container port? And is the server process inside the container listening on 0.0.0.0 (every network interface it has) rather than 127.0.0.1? That last one catches a lot of people. 127.0.0.1 is the loopback address, a machine talking to itself, so a service bound there inside a container is answering nobody but that container.
The name lookup is served by a resolver Docker embeds in the network itself, not by anything you installed in the image. When a name refuses to resolve, two commands do most of the work: docker network inspect shows you which containers are genuinely attached, and docker exec ... getent hosts othername asks the resolver, from inside a running container, what that name comes back as. Keep container IPs out of your config, because they move. The container name, and later the Compose service name, is the part that holds still.
One last suspect for the bug that only happens on your machine. Desktop firewalls and corporate VPN clients (virtual private network software that pushes your traffic through the company's network) both rewrite routing rules, and they sometimes flatten Docker's bridge on the way past. If the same commands work for a colleague and fail for you, look at what your laptop has installed before you start rereading your Compose file. Ruling that out early saves an afternoon spent debugging application code that was never wrong.
Try this
Build a network of your own, attach two Alpine containers, and have one of them resolve the other by name.
docker network create learnnetdocker run -d --rm --name alpha --network learnnet alpine:3.20 sleep 600docker run --rm --network learnnet alpine:3.20 nslookup alphadocker rm -f alphadocker network rm learnnet
dbf28adc8dbe418c23a1de269e1cd378a3e3221b8eefdfa3a7b3916dc3e1e394b6ca6815869b78aefcb976aedf3b898926800fcb7b20d69661b7db097800662cServer: 127.0.0.11Address: 127.0.0.11:53Non-authoritative answer:Name: alphaAddress: 172.18.0.2Non-authoritative answer:alphalearnnet
Takeaway
A network you create yourself is what gives containers names for each other. -p is what gives the host a way in. Inside appnet, web reaches db by name on the container port, and outside the host, only what you published ever answers.