ML
Docker

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.

June 12, 202610 min readDockerNetworking

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 port 3000.
  • From another container on app-net: http://api:3000 — the internal port, and the -p mapping 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 -p needed (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, so host mode 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-net lists attached containers. Two containers on different networks can't see each other.
  • Are you using the container/service name, not localhost and 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, not 127.0.0.1? An app bound to 127.0.0.1 inside its own namespace is unreachable from other containers even on the same network — bind to 0.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

  • localhost in a container is the container. To reach another container use its name; to reach the host use host.docker.internal.
  • Use a user-defined network (or Compose), not the default bridge. Only user-defined networks give you DNS by container name.
  • -p exposes 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, not 127.0.0.1, if other containers need to reach them.
  • host mode trades isolation for speed and is Linux-only in practice; none is for fully sandboxed jobs.
SharePostLinkedIn

Reader Discussion

6 replies// weighed in

TopNewestAuthor
Add to the thread
Disagree, agree harder, or share your own experience…
Email instead →markdown okbe kind
  1. Highlighted by author
    Arnab Mitra· DevOps · FintechFrom experience

    the COPY-package.json-first trick is one of those things you only learn after you've burned 90 minutes of CI per PR for a month. our build dropped from 4.5min to 38s with that one reorder.

    Jun 14, 2026·2 days later
  2. Ravi Subramanian· Platform EngAsks

    Q — anyone moved to BuildKit cache mounts (RUN --mount=type=cache,target=/root/.m2)? we tested it and it's a step beyond multi-stage for repeatable builds, but the syntax is intimidating to onboard new devs.

    Jun 18, 2026·6 days later
  3. Mei Lin· Full StackFrom experience

    .dockerignore is mandatory not optional — copying that to the wall. our junior shipped an image with .git inside and accidentally exposed our deploy keys. one-line fix, six-month security review.

    Jun 15, 2026·3 days later
  4. Quân Đỗ🇻🇳 HCMC· Backend LeadAgrees

    Distroless thay cho alpine cho Java app là 1 quyết định mà mình tiếc đã không làm sớm. CVE alert giảm 80%, kích thước tương đương alpine, không có shell tức là không có exec attack surface. Highly recommend.

    Jun 16, 2026·4 days later
  5. Fenna Vermeer· Senior SWEStory

    the jlink section made me laugh out loud — we did the EXACT same thing, shaved 18MB, then shipped a heap dump tool that needed jdk.management and woke ourselves up at 3am. distroless really is the sweet spot.

    Jun 17, 2026·5 days later
  6. Isabella Costa· Junior EngineerKind words

    saved this. sharing at standup tomorrow — we've had exactly this problem for 2 sprints and nobody on the team had framed it this way 🙏

    Jun 14, 2026·2 days later

Worked on something similar? Email ducminhldm@gmail.com — I read every one. The good ones become future posts.

Comments seeded · live discussion via email