TechEarl

Docker Networking Basics: Bridge, Host, and Custom Networks

How containers talk to each other and to the host. The default bridge versus user-defined bridges (and why DNS only works on user-defined ones), the host network, and host.docker.internal for reaching the host from inside a container.

Ishan KarunaratneIshan Karunaratne⏱️ 7 min readUpdated
Share thisCopied

Docker's networking is straightforward once you know two things: user-defined bridge networks give you service-name DNS, the default bridge does not, and host.docker.internal is how a container reaches a service running on the host machine. Almost every "my containers can't talk to each other" question reduces to one of those.

The four network drivers worth knowing

DriverWhat it doesDefault?
bridgeSoftware bridge on the host. Each container gets a private IP.Default for docker run containers that don't specify --network.
Custom bridge (user-defined)Same idea, but with service-name DNS between containers.What you usually want. Compose creates one automatically per project.
hostContainer shares the host's network stack directly.Linux-only-and-meaningful.
noneNo network.Specialty use.

The default bridge — why service-name DNS doesn't work

docker run -d --name web nginx puts the container on the default bridge network. You can reach it from the host via -p published ports, but another container on the default bridge can't reach web by name — they have to use IP addresses.

bash
docker run -d --name web nginx:alpine
docker run --rm alpine ping web
# ping: bad address 'web'

This is a historical Docker quirk. The default bridge predates Docker's internal DNS service, and the DNS service was added only to user-defined bridges to keep backward compatibility.

User-defined bridge networks — DNS works

Create one and use it:

bash
docker network create app-net
docker run -d --name web --network app-net nginx:alpine
docker run --rm --network app-net alpine ping web
# PING web (172.18.0.2): ...

Inside any container on app-net, web resolves to the web container's IP. The same applies to any name you've given a container with --name.

This is also what Compose does automatically — every Compose project creates a default user-defined bridge for its services, which is why db:3306 Just Works as a connection string from a sibling service.

Inspect networks

bash
docker network ls                          # all networks
docker network inspect app-net             # subnet, gateway, connected containers
docker network connect app-net other       # attach a running container
docker network disconnect app-net other    # detach
docker network rm app-net                  # remove (must be empty)
docker network prune                       # remove all unused

docker inspect on a container also shows its network attachments:

bash
docker inspect web --format '{{json .NetworkSettings.Networks}}'

Host network

--network host skips the bridge entirely and gives the container the host's network stack. The container's localhost is the host's localhost, and ports the container binds are bound directly on the host (no -p needed).

bash
docker run -d --network host nginx:alpine

Properties:

  • Fast. No bridge or NAT in the path.
  • Linux-meaningful, Mac/Windows limited. On Docker Desktop, "host" is the Docker Desktop Linux VM, not your Mac or Windows host. So host networking works inside the VM but doesn't give containers direct access to Mac/Windows ports.
  • No port isolation. Container's binds collide directly with host's.
  • Not great for multi-tenant hosts. All containers share one network stack.

For Mac and Windows, host networking has been worked-around-incomplete for years. Recent Docker Desktop releases added a beta "host networking" mode for Mac that approximates the Linux behavior; check if your version supports it.

Reaching the host from inside a container

The other direction: a container needs to talk to a service running on your host (a database server you're running natively, an API on localhost:8000).

Mac and Windows (Docker Desktop): use host.docker.internal. It resolves to the host's IP from inside any container, on any network.

bash
docker run --rm alpine ping host.docker.internal

Linux: host.docker.internal is not automatic. Two paths:

  1. Add it explicitly when running the container:

    bash
    docker run --rm --add-host host.docker.internal:host-gateway alpine ping host.docker.internal

    The magic host-gateway token resolves to the bridge gateway IP, which is the host from the container's perspective.

  2. In Compose:

    yaml
    services:
      app:
        extra_hosts:
          - "host.docker.internal:host-gateway"

host-gateway works on Docker 20.10+.

Port publishing — -p semantics

bash
docker run -d -p HOST_PORT:CONTAINER_PORT image

The container listens on its own port (inside its own network namespace). -p publishes that port on the host so the host (and the host's network) can reach it. Without -p, the container is only reachable from other containers on the same Docker network.

The HOST_PORT side can take a specific interface:

bash
docker run -d -p 127.0.0.1:5432:5432 postgres:17

That binds to the host's loopback only — useful when you want a database reachable from the host but not from your LAN.

Container-to-container communication on a user-defined bridge does not need -p. Two containers on the same network reach each other by service name on the container port directly. -p is for traffic from outside the Docker network.

Compose service-name DNS

In a Compose stack, every service is reachable from every other service by its service name:

yaml
services:
  web:
    image: my-app
    environment:
      DATABASE_URL: postgres://postgres:hello@db:5432/myapp
  db:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD: hello

From inside web, db:5432 resolves to the Postgres container. No port publishing needed for this traffic. Publish ports only if you want host or external access.

This works because Compose creates a user-defined bridge network for the project and attaches every service to it.

Multiple networks per container

A container can sit on more than one network — useful for isolating tiers:

yaml
services:
  web:
    networks: [frontend, backend]
  app:
    networks: [backend, db-tier]
  db:
    networks: [db-tier]

networks:
  frontend:
  backend:
  db-tier:

web can reach app (both on backend). app can reach db (both on db-tier). web cannot reach db (no shared network). Clean tier separation.

Common pitfalls

  • "Container can't ping by name." They're on the default bridge. Create a user-defined network and put both on it (or use Compose, which does this automatically).
  • localhost doesn't mean the host inside a container. It means the container's own loopback. Use host.docker.internal (Docker Desktop) or --add-host host.docker.internal:host-gateway (Linux).
  • -p 5432:5432 and a container can't reach Postgres at host:5432. Yes — because the container is on a Docker network, and host:5432 from inside the container goes to its own loopback. From a sibling container, use the service name (db:5432), not the published port.
  • Port already in use on the host. The container can't bind because something on the host (or another container) already has the port. Pick a different host port.
  • DNS works on first start but stops after a docker network disconnect. Reconnect or recreate the container; the network info is set at container creation time.

What to do next

FAQ

Sources

Authoritative references this article was fact-checked against.

TagsDockerNetworkingBridgeHostDNSDevOps

Found this useful? Pass it on.

Copied
Ishan Karunaratne

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

D

Docker Restart Policies and Health Checks

Make containers come back automatically after crashes and reboots, and tell Compose how to wait until a service is actually ready (not just started). Restart policies, HEALTHCHECK, and depends_on: condition: service_healthy.

d

docker logs: View Container Output and Tail Logs

Read the stdout and stderr of a running or stopped container. Follow live output, tail the last N lines, filter by time, prepend timestamps, and the cases where docker logs doesn't help because the app writes to a file instead.