Docker Networking: Bridge, Host, and How Containers Actually Talk
localhost inside a container is the container, not your host — and that one fact explains most Docker networking confusion. A tour of bridge, host, none, and user-defined networks.
Most Docker networking confusion collapses into a single misunderstanding: localhost inside a container refers to the container itself, not your host machine. Once that clicks, the rest — why your app can't reach the database, why -p is needed, why container names sometimes resolve and sometimes don't — falls into place. Here's the model.
1. Every container gets its own network namespace
When Docker starts a container it creates a Linux network namespace for it: a private network stack with its own interfaces, routing table, and 127.0.0.1. That isolation is the whole point — but it means localhost in the container is a different loopback than localhost on the host or in any other container.
# inside the container, this is the CONTAINER's localhost
curl http://localhost:5432 # NOT your host's Postgres
So "my Node app connects to localhost:5432 and it works on my laptop but not in Docker" is expected: in the container there's nothing on its own localhost:5432. The fix is never "localhost" — it's addressing the other namespace correctly, which is what the network drivers below are for.
2. The default bridge: connected, but no name resolution
By default a container joins the built-in bridge network (docker0). Docker creates a virtual ethernet pair per container, assigns it a private IP (e.g. 172.17.0.x), and NATs its outbound traffic. Containers on the default bridge can reach each other by IP, but — and this trips everyone up — the default bridge does not give you DNS by container name.
docker run -d --name db postgres
docker run -it --rm alpine ping db # FAILS on the default bridge
You'd have to hard-code the container's IP, which changes on restart. That's almost never what you want.
3. User-defined bridge networks: the right default
Create your own bridge network and containers on it get automatic DNS by container name. This is the single most useful thing to know in Docker networking:
docker network create app-net
docker run -d --name db --network app-net postgres
docker run -d --name api --network app-net my-api
# inside the api container, this just works:
# postgres://db:5432 <-- "db" resolves to the db container's current IP
Docker runs an embedded DNS server (at 127.0.0.11 inside each container) that resolves container names and network aliases on user-defined networks. This is also exactly what Docker Compose does for you — every service in a Compose file lands on a shared user-defined network and is reachable by its service name. So in Compose, an app talks to db:5432, never localhost:5432.
Rule: for multi-container apps, always use a user-defined network (or Compose) and address services by name. Never the default bridge, never hard-coded IPs.
4. Publishing ports: -p is host↔container, not container↔container
Containers on the same user-defined network reach each other on the container's own port with no -p at all. -p is only about exposing a port to the host (and the outside world):
docker run -d --name api --network app-net -p 8080:3000 my-api
- From your laptop/browser:
http://localhost:8080→ forwarded to the container's port3000. - From another container on
app-net:http://api:3000— the internal port, and the-pmapping is irrelevant.
A classic mistake is one container trying to reach another via localhost:8080 because that's the published port. Wrong on two counts: localhost is the wrong namespace, and 8080 is the host-side port. Use api:3000.
5. Reaching the host from inside a container
Sometimes the container genuinely needs to reach a service running on the host (a database on your laptop, another dev server). localhost won't do it. Docker provides a special DNS name:
# works on Docker Desktop (macOS/Windows) and modern Docker Engine
curl http://host.docker.internal:5432
On Linux Engine you may need to add it explicitly with --add-host=host.docker.internal:host-gateway. This is the correct way to bridge into the host's network namespace from a container.
6. host and none: the two extremes
Two other drivers exist for the edge cases:
--network host— the container shares the host's network namespace directly: no isolation, no NAT, no-pneeded (the container binds host ports directly). It's faster (no NAT hop) and occasionally necessary, but you lose port isolation and container-name DNS. Note it's effectively Linux-only — on Docker Desktop for macOS/Windows the container runs inside a VM, sohostmode does not behave like true host networking.--network none— no networking at all beyond loopback. Useful for fully sandboxed, compute-only jobs that should never touch the network.
7. A quick debugging checklist
When "container A can't reach container B":
- Are they on the same user-defined network?
docker network inspect app-netlists attached containers. Two containers on different networks can't see each other. - Are you using the container/service name, not
localhostand not an IP? - Are you hitting the internal port (the one the app listens on), not the published host port?
- Is the target actually listening on
0.0.0.0, not127.0.0.1? An app bound to127.0.0.1inside its own namespace is unreachable from other containers even on the same network — bind to0.0.0.0.
That last one is sneaky and common: a service configured to listen on localhost only accepts connections from its own namespace, so the network is fine but the bind address isn't.
Rules of thumb
localhostin a container is the container. To reach another container use its name; to reach the host usehost.docker.internal.- Use a user-defined network (or Compose), not the default bridge. Only user-defined networks give you DNS by container name.
-pexposes a port to the host, not to other containers. Container-to-container uses the internal port directly, no publishing required.- Bind services to
0.0.0.0, not127.0.0.1, if other containers need to reach them. hostmode trades isolation for speed and is Linux-only in practice;noneis for fully sandboxed jobs.