Code Safari

Chapter 123·Intermediate·9 min read

Networking: How Containers Talk

A plain-English guide to Docker networking — why each container is isolated, publishing a port to reach one from your host, how containers on a shared network find each other by name, the bridge network, and why 'localhost' inside a container isn't what you think.

July 19, 2026

Every container so far has run in isolation — a single sealed bubble. But real systems are never one container. A typical app is a web server talking to a database talking to a cache, each in its own container, all needing to find and reach each other. This chapter is how that happens: how you reach a container from the outside, how containers talk among themselves, and the one misunderstanding — about localhost — that trips up nearly every beginner. It leans directly on the ports material from the Linux guide, now applied across container walls.

Isolated by default

The first thing to internalise: a container's network is isolated. By default, a container gets its own private network space — its own network interfaces, its own set of ports, walled off from your host and from other containers. A web server listening on port 80 inside a container is genuinely listening, but that port is sealed inside the bubble. Your browser on the host can't reach it, and neither can another container, until you deliberately open a path.

This is a security feature, not an obstacle. Nothing is exposed by accident; you connect exactly what you mean to connect and nothing else. There are two distinct connection problems to solve, and Docker handles them differently:

  1. Outside → in: letting something on your host (a browser) reach a container. This is publishing a port.
  2. Container → container: letting two containers talk to each other. This is a shared network.

Keep those two apart in your head — they're solved by different mechanisms, and conflating them is the root of most confusion.

Publishing a port: reaching a container from your host

You've already met the tool for problem one — the -p flag from chapter two. Publishing a port pokes a hole through the container's isolation, connecting a port on your host to a port inside the container:

$ docker run -d -p 8080:80 nginx

The mapping is always -p host:container. Here, port 8080 on your machine is wired to port 80 inside the container. So a browser visiting localhost:8080 on your host reaches nginx, which is actually listening on port 80 within its bubble. The two numbers can be anything — -p 3000:3000 maps them one-to-one; -p 8080:80 translates between them.

Browser → localhost:8080 (host)
Docker forwards through the mapping
Port 80 inside the container
nginx responds
Publishing wires a host port to a container port

Without -p, the container runs fine but is unreachable from your host — the app is up, listening, and completely sealed off. "My server is running but I can't open it in the browser" is, nine times out of ten, a forgotten -p. Recall too from the Dockerfile chapter that EXPOSE documents a port but does not publish it; -p at run time is what actually opens the door.

Container-to-container: a shared network with names

Now problem two, and the genuinely clever part. When containers are placed on the same Docker network, they can reach each other directly — and, best of all, by name. Docker runs a small internal DNS server on its networks, so a container can connect to another simply by using its name as the hostname.

Create a network and attach containers to it:

$ docker network create appnet
 
$ docker run -d --name db --network appnet postgres:16
$ docker run -d --name web --network appnet -p 8080:80 myapp

Both containers are on appnet. Now, from inside the web container, the database is reachable at the hostname db — literally the container's --name. The app's configuration would point at db:5432 (the container name, then Postgres's port), and Docker's DNS resolves db to the database container's internal address automatically. No IP addresses to look up, no hardcoding — you refer to other containers by the names you gave them.

This is the mechanism that turns a pile of separate containers into a system. The web container finds the database by name; the database finds nothing it doesn't need to; the cache is reachable at redis. Each container stays independently built and deployed, yet they compose into one working application over the private network.

The localhost trap

Here is the mistake almost everyone makes once, and understanding it now will save you a baffling afternoon. Inside a container, localhost means the container itself — not your host machine, and not any other container.

localhost (and 127.0.0.1) always means "this very machine, right here." The trouble is that each container is its own "machine" as far as the network is concerned. So when code inside your web container tries to connect to a database at localhost:5432, it's looking for a database inside the web container — where there isn't one. The database is in a different container. It fails, confusingly, because on your laptop (without Docker) localhost:5432 would have worked fine.

   WRONG                              RIGHT
   web container                      web container
   connect → localhost:5432           connect → db:5432
             │                                  │
             ▼                                  ▼
   (looks inside web —              db container (Docker DNS
    nothing there) ✗                resolves the name) ✓
Why localhost fails across containers

The fix is everything from the previous section: containers reach each other by container name over a shared network, never localhost. Your app connects to db:5432, not localhost:5432. The rule to carry: the moment your database and app are in separate containers, localhost between them is wrong — use the other container's name.

The bridge network underneath

You don't strictly need the internals, but a quick peek makes the behaviour concrete. By default, Docker connects containers to a virtual bridge network — think of it as a private LAN running on your host. Each container gets an internal IP on that LAN and can talk to others on it, while the bridge insulates them from the outside world. Publishing a port with -p is really Docker forwarding traffic from your host into that private LAN.

The one practical wrinkle worth knowing: Docker's default bridge doesn't give you the name-based DNS — that convenience comes on user-defined networks, the ones you make with docker network create (or that Compose makes for you). That's precisely why the recommended pattern is to create your own network rather than rely on the default: on a network you defined, containers find each other by name, and everything just works. In practice you'll rarely run these network commands by hand, because the next chapter's tool does it all automatically — but knowing what it's doing under the hood means you'll never be mystified by it.

From commands to configuration

You can now connect containers into a real system: publish the ports that need outside access, put your containers on a shared network so they find each other by name, and avoid the localhost trap by always addressing other containers by their names. That's the complete conceptual toolkit for multi-container apps.

But look at what running even a modest system already takes — a docker network create, then a long docker run for the database with its volume and network, then another for the app with its ports and network, every time, in the right order, remembered perfectly. That's tedious and error-prone. The next chapter introduces Docker Compose, which lets you describe your entire multi-container app — every service, network, volume, and port — in a single file, and bring the whole thing up with one command. Everything you just learned by hand is what Compose quietly does for you.

Networking: How Containers Talk | Code Safari