DevTools Hub

Search tools

Search for a developer tool

Understanding Docker Networks

Docker networking mostly stays invisible right up until two containers can't reach each other and it's not obvious why. Most of that confusion collapses into a handful of concrete facts once you know where to look — this covers the ones that actually explain the errors you'll hit.

The default bridge network doesn't do name resolution

Every Docker install has a network literally named bridge, and a plain docker run with no --network flag lands a container on it. Two containers on that default bridge can reach each other by IP address — but not by container name. There's no built-in DNS resolution between containers on the default bridge network; that's a deliberate, long-standing Docker design decision, not a bug.

A user-defined bridge network — one you create yourself with docker network create, or that Docker Compose creates for you — behaves differently: containers on it get automatic DNS resolution by container name (or Compose service name) via Docker's embedded DNS server. This is the entire reason a service in a docker-compose.yml can connect to db as a hostname without any manual network configuration — Compose always creates a project-scoped user-defined network, never uses the bare default bridge.

What Compose actually sets up

Every service in a docker-compose.yml with no networks: of its own lands on an implicit network literally named default — still a real user-defined bridge network, just one Compose creates automatically instead of you naming it. Declaring networks explicitly is how you split services into groups that can't reach each other at all:

services:
  web:
    image: nginx:latest
    networks:
      - frontend
  api:
    build: ./api
    networks:
      - frontend
      - backend
  db:
    image: postgres:16
    networks:
      - backend

networks:
  frontend:
  backend:

Here, web can reach api (both on frontend), and api can reach db (both on backend) — but web has no network in common with db and can't reach it at all, even though they're defined in the same file. Network membership, not file proximity, is what determines reachability.

Container-to-container traffic never touches the published host port

This is the single most common source of "why can't my app connect to the database" confusion. A ports: mapping like "5433:5432" only matters for reaching the container from the host machinelocalhost:5433 from your laptop, in this example. Another container on the same Docker network reaches it directly on the container's own port, ignoring the host mapping entirely:

# From the host machine:
psql -h localhost -p 5433 ...   # uses the published mapping

# From another container on the same network:
psql -h db -p 5432 ...          # uses the container's real port — 5433 means nothing here

A service that only ever talks to other containers doesn't need a ports: entry at all — that's purely for host access.

EXPOSE in a Dockerfile doesn't publish anything

EXPOSE 5432 in a Dockerfile is documentation — it tells a human (or tooling) which port the process inside listens on. It doesn't open, forward, or publish anything by itself; only an explicit -p flag on docker run, or a ports: entry in Compose, actually binds a port to the host. A missing EXPOSE also doesn't block anything — containers on the same network can already reach any port the process is actually listening on, declared or not.

Host networking mode skips all of this

network_mode: host (or docker run --network host) puts the container directly on the host's network stack — no isolation, no published-port mapping, no container DNS. The process inside binds host ports directly, the same as if it ran outside a container. It trades away network isolation for a small performance edge and is Linux-specific in practice (Docker Desktop on Mac and Windows doesn't give it the same meaning, since the Docker daemon itself already runs inside a VM there).

Try it yourself

Docker Compose Visualizer renders exactly the network-membership picture this post describes — paste a compose file and see which services actually share a network, not just which ones are defined near each other. If you're not sure a network reference is even valid, Docker Compose Validator flags a service that references a network never declared in the top-level networks: section. Both run entirely in your browser.

Related tools