NovuSpark
All articles

January 30, 2026 · NovuSpark Team

Docker Networking Explained

This is the third post in our Docker fundamentals series, building on images and containers and production Dockerfiles.

A question almost everyone new to Docker eventually asks in some form: "why can't my container connect to localhost:5432 when Postgres is clearly running on my machine?" The answer is that a container has its own network namespace — its own localhost, entirely separate from the host's. Understanding Docker's network drivers is what turns that from a confusing surprise into an expected, predictable behavior.

The default: bridge networking

docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
a1b2c3d4e5f6   bridge    bridge    local

Every container, unless told otherwise, attaches to bridge — a private virtual network Docker creates on the host. Containers on this network get their own internal IP addresses and can reach each other by IP, but not by container name, and the host's own localhost is not the same thing as any container's localhost.

docker run -d --name web -p 8080:80 nginx

-p 8080:80 publishes the container's port 80 to the host's port 8080 — this is specifically what makes localhost:8080 on your actual machine reach the container. Without a published port, a container on the default bridge network is reachable from other containers on that network, but not from the host machine directly.

User-defined bridge networks: the practically important upgrade

docker network create app-network
 
docker run -d --name db --network app-network postgres:16
docker run -d --name web --network app-network -p 8080:80 my-app

Containers on a user-defined bridge network get something the default bridge doesn't provide: automatic DNS resolution by container name. From inside the web container, db resolves directly to the database container's current IP address:

docker exec web ping db
PING db (172.20.0.2): 56 data bytes
64 bytes from 172.20.0.2: seq=0 ttl=64 time=0.089 ms

This is the detail that makes multi-container applications actually pleasant to configure — an application's database connection string can simply say db as the hostname, and it keeps working correctly even if the database container is removed and recreated with a new internal IP address, because DNS resolution happens by name, dynamically, every time.

This is also the practical reason to always create a dedicated network for a multi-container application, rather than relying on the default bridge: name-based resolution, and isolation from unrelated containers that happen to also be running on the same host.

Host networking: removing the isolation entirely

docker run -d --network host nginx

With --network host, a container shares the host's network namespace directly — no isolation, no published ports, no virtual bridge. The container's port 80 is the host's port 80. This trades away Docker's networking isolation entirely, in exchange for eliminating the small amount of overhead the bridge network adds — a trade only worth making for genuinely performance-sensitive services where that overhead has actually been measured and matters, not as a default convenience.

Container-to-container communication in practice

# A preview of Compose syntax — covered fully in the next post in this series
services:
  web:
    build: .
    ports:
      - "8080:80"
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: example
# Inside the web container's application code
DATABASE_URL = "postgresql://postgres:example@db:5432/mydb"

db here is not a placeholder — it's the actual, literal hostname the application connects to, resolved automatically because Compose creates a user-defined network for every project by default and names each service's container after its key in the Compose file. This is the payoff of everything covered above, in the form it actually shows up in day-to-day work.

Common pitfalls worth naming directly

  • Expecting localhost inside a container to mean the host machine. It doesn't — it means the container's own network namespace. host.docker.internal (on Docker Desktop) is the actual mechanism for a container to reach a service running on the host machine itself.
  • Forgetting that stopping a container doesn't free its published port immediately in every case — a container that failed to shut down cleanly can leave a port bound longer than expected, which reads as "the port is already in use" and confuses people into thinking something else is wrong.
  • Assuming containers on the default bridge network can resolve each other by name. They can't — DNS-by-name is specifically a user-defined network feature, one more reason to avoid the default bridge for anything beyond quick, single-container testing.

What to actually remember from this post

  • Every container gets its own network namespace — its localhost is never the same as the host's localhost.
  • -p publishes a container's port to the host; without it, a container on the default bridge is reachable from other containers but not from the host directly.
  • User-defined bridge networks provide DNS resolution by container name — the default bridge does not, which is the main practical reason to always create a dedicated network per application.
  • --network host removes isolation entirely — a deliberate trade for measured performance needs, not a default.

Next in the series: Docker Compose for Local Multi-Container Development, where these networking concepts become a single declarative file instead of a sequence of manual docker run and docker network commands.

Ready when you are

Want training built around your team's real work?

Tell us about your team and what you're trying to solve — we'll recommend a program that fits.