CoursesDocker for beginnersContainer networking basics

Container networking basics

bridge, host, none, and publishing ports.

Beginner12 min · lesson 13 of 16
In plain terms
Containers on the same named network are like phones saved in each other’s contacts — they dial each other by name instead of memorizing numbers (IP addresses) that keep changing. Publishing a port is like giving one of them a public phone number the outside world can call.

Two containers can run on the same laptop, started seconds apart, and still fail to find each other. Almost everyone walks into that wall once. Every container comes up with a network address of its own, handed out fresh the way a swimming pool hands you whichever locker happens to be free that day. The technical name for it is a private IP address (Internet Protocol address, the number other machines use to find it on a network). Tomorrow you get a different locker. Restart the container and you get a different address. Type one into your app's config and it will be wrong by next week. Docker's answer is to put your containers on a shared network and let them call each other by name, the way you tap a saved contact instead of recalling ten digits.

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.

terminal
$ docker run -d --name web nginx:alpine
$ docker run --rm busybox ping -c 1 web
output
d9b1c0f7a4e83b2c6f1a0d5e9c8b7a6f4e3d2c1b0a9f8e7d6c5b4a3f2e1d0c9b
ping: bad address 'web'

Read that second line slowly. busybox could not turn the name web into an address, so it never sent a single packet. The web container is up and perfectly healthy. That is not what broke. The default bridge ships without a phone book. The proper name for that phone book is DNS (Domain Name System, the service that turns a name into an address), and Docker does not run one on the default bridge. Containers there can still reach each other by raw IP. Nobody is writing the names down.

Give your containers a network of their own

Create your own bridge network and Docker quietly starts a small DNS server that serves that network alone. Every container you attach gets written into the phone book under its --name, and its neighbours can look it up. You configure nothing. The phone book arrives with the network. Run the same test again, this time with both containers sitting on a network you made.

terminal
$ 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
output
web
a3f21c9d7b4e6082f5a1c0d9e83b746024c1f8a9d0e73b645c2b1a09f8e7d6c3
b81f4c0a9e2d7635c8a0f4e1d2b709536f1a8c4e07d3b9625a1c8e40f7b0d3a6
PING web (172.18.0.2): 56 data bytes
64 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 loss
round-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.

terminal
$ docker run --rm --network appnet busybox wget -qO- http://web
output
<!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 and
working. 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.

terminal
$ docker run -d --name web-public -p 8080:80 --network appnet nginx:alpine
$ curl -s http://localhost:8080 | head -n 4
output
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.

Port already in use
Publishing needs a free port on the host, and grabbing the same one twice is a classic first stumble. Start a second container, call it web2, with -p 8080:80 while the first is still holding 8080, and the Docker daemon (the background program that runs your containers) turns you down: "Error response from daemon: driver failed programming external connectivity on endpoint web2: Bind for 0.0.0.0:8080 failed: port is already allocated." Nothing is broken. Run docker ps to see who already owns 8080, then either stop that container or publish to a free host port, like -p 8081:80. The container port on the right stays 80 either way.
How traffic reaches your containers
web and db find each other by name across the private network. Only web is published, so only web can be reached from outside the host.

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.

A published port is a hole you cut in your own wall. It is how the host, and often anyone who can reach the host, gets to a service, so cut it where you need it and nowhere else. By default -p 8080:80 binds every network interface on the machine, so on coffee shop wifi you have just offered that port to the coffee shop; write -p 127.0.0.1:8080:80 and only the local machine can reach it. Docker also writes its own firewall rules into the kernel, and they are read before the ones a host firewall like ufw or firewalld manages, so a firewall that looks closed usually does not cover a published port at all. New Docker users tend to publish every container to be safe, and end up with a database listening on the laptop's own network for no reason at all. Service-to-service calls inside a network you created never needed it; those containers talk on container ports directly.

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.

terminal
docker network create learnnet
docker run -d --rm --name alpha --network learnnet alpine:3.20 sleep 600
docker run --rm --network learnnet alpine:3.20 nslookup alpha
docker rm -f alpha
docker network rm learnnet
output
dbf28adc8dbe418c23a1de269e1cd378a3e3221b8eefdfa3a7b3916dc3e1e394
b6ca6815869b78aefcb976aedf3b898926800fcb7b20d69661b7db097800662c
Server: 127.0.0.11
Address: 127.0.0.11:53
Non-authoritative answer:
Name: alpha
Address: 172.18.0.2
Non-authoritative answer:
alpha
learnnet

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.

Quick check
01The web container is up and healthy, but docker run --rm busybox ping -c 1 web answers with bad address 'web'. Neither command named a network. Which change gets the name working?
Correct — docker network create appnet gives you a bridge network with Docker's own DNS attached to it. Put both containers on it and the name web maps to whatever address that container is holding right now.
Incorrect — -p opens a door from the host into a container. It teaches no container the name of any other container, and this ping failed before a port was ever involved.
Incorrect — Host networking drops the container onto your machine's network. It hands out no container names, and you give up the isolation the bridge was providing.
Incorrect — Raw IPs do work on the default bridge, so this looks fine today and is wrong by next week. Addresses get handed out fresh on every restart, which is the whole reason to use names.
02You are running a web container your browser needs to load and a db container that only the web app talks to. Both sit on a network you created. Which one do you publish with -p?
Incorrect — -p has nothing to do with name lookups. Publishing db just puts your database on the laptop's own network for no reason, which is the habit new Docker users fall into.
Correct — The browser has to get in from outside the host, so web is where you cut the hole. The app talks to db over the network you created, by name, on the container port.
Incorrect — Host mode shares the machine's whole network stack, costs you the isolation, and makes the setup harder to move between machines. It also hands out no container names.
Incorrect — Container addresses move on every restart, so that config goes stale fast. The container name, and later the Compose service name, is the part that holds still.
03web and db are both attached to appnet. From inside web, docker exec ... getent hosts db comes back with an address, but the app still cannot connect to the database. What is the likeliest cause?
Incorrect — -p is only about traffic arriving from outside the network Docker built. Containers on the same network reach each other on container ports with no publishing in the command at all.
Incorrect — The default bridge runs no DNS, so a name there resolves to nothing. An address came back, which tells you both containers really are attached to the network you created.
Incorrect — Docker's embedded resolver answers with the address that container is holding at that moment, so there is nothing stale to clear. A restart would only hand out a new address.
Correct — 127.0.0.1 is a machine talking to itself, so a service bound there is answering nobody but its own container. Bind it to 0.0.0.0 and it listens on every interface, where its neighbours can reach it.

Related