Skip to content
C Codeloom
Docker

Docker Networking Basics: Bridges, Ports, and DNS

A practical guide to Docker networking: the default bridge, user-defined bridges, host networking, container DNS, and how port publishing actually works.

·6 min read · By Yash Kesharwani
Intermediate 10 min read

What you'll learn

  • How Docker creates and uses the default bridge network
  • Why user-defined bridges are almost always a better choice
  • When to use host networking and what it gives up
  • How container DNS resolution works between services
  • What port publishing actually does at the iptables level

Prerequisites

  • A working Docker install — see [Docker install and first container](/blog/docker-install-and-first-container)
  • Familiarity with [Dockerfile basics](/blog/dockerfile-basics)

Once you move beyond a single container, networking quickly becomes the topic that decides whether things work or do not. The good news: Docker’s networking model is small. There are a handful of drivers, a few key commands, and once you understand how name resolution works inside a user-defined bridge, most of the confusion disappears.

This guide focuses on the three network modes you will use most: the default bridge, user-defined bridges, and host networking — plus the DNS and port publishing details that tie them together.

The default bridge

When Docker starts, it creates a network named bridge. Every container you run without specifying --network joins it. You can see it with:

docker network ls
docker network inspect bridge

The default bridge has a few quirks that catch people off guard:

  • Containers on it can reach each other by IP, but not by name. There is no built-in DNS for the default bridge.
  • The only way to get name-based communication is the legacy --link flag, which is deprecated.
  • Every container gets an IP from the bridge’s subnet (typically 172.17.0.0/16).

For one-off docker run commands the default bridge is fine. For anything with two or more containers that need to talk to each other, switch to a user-defined bridge.

User-defined bridges

A user-defined bridge is a network you create explicitly. It uses the same bridge driver, but it gains a critical feature: automatic DNS-based service discovery.

docker network create app-net

docker run -d --name db --network app-net postgres:16
docker run -d --name api --network app-net -e DB_HOST=db myorg/api:latest

The api container can connect to db:5432 directly. Docker runs an embedded DNS server at 127.0.0.11 inside each container, and it resolves container names on the same network to their current IP addresses. No /etc/hosts hacks, no link flags.

Other benefits of user-defined bridges:

  • Isolation. Containers on app-net cannot reach containers on other-net unless they are attached to both.
  • Attach and detach at runtime with docker network connect and disconnect.
  • Better defaults — fewer kernel-level iptables rules per container.

When you use Docker Compose, every project gets a user-defined bridge automatically, named after the project. This is why services in a Compose file can reach each other by their service name with no extra configuration.

Host networking

The host network driver removes network isolation between the container and the Docker host. The container shares the host’s network stack: same interfaces, same IPs, same ports.

docker run --rm --network host nginx

That nginx is now listening on the host’s port 80 directly — there is no -p flag involved. Host networking is useful when:

  • You need the lowest possible network overhead (no NAT, no veth pair).
  • You are running an application that needs to see real client IPs without proxy protocol.
  • You need a wide range of ports and listing them with -p is impractical.

The trade-offs:

  • No port remapping — if the host is already using 80, the container will fail to bind.
  • No isolation — a misbehaving container can fight with host services for ports and interfaces.
  • Not supported on Docker Desktop for Mac and Windows in the same way it works on Linux. Behavior differs across platforms.

Reach for host networking deliberately, not as a default.

How port publishing actually works

When you run docker run -p 8080:80 nginx, Docker does two things:

  1. Connects the container to a bridge network (default bridge unless you say otherwise).
  2. Adds iptables rules in the host’s nat table that DNAT incoming traffic on host port 8080 to the container’s port 80.

That second step is what makes the container reachable from outside the host. You can see the rules with iptables -t nat -L -n on Linux.

The forms of -p:

  • -p 8080:80 — publish container port 80 on host port 8080, all interfaces.
  • -p 127.0.0.1:8080:80 — bind only to localhost on the host.
  • -p 80 — publish to a random high host port, see it with docker port.
  • -p 8080:80/udp — explicitly UDP.

Crucially, port publishing is only needed for traffic from outside the Docker network. Containers on the same user-defined bridge talk to each other on container ports directly — no -p required between them.

A common mistake: publishing database ports to 0.0.0.0 in production. If the container is only consumed by other containers on the same network, do not publish at all. If you must expose for local development, bind to 127.0.0.1 so only your machine can connect.

Container DNS in detail

Inside any container on a user-defined bridge, /etc/resolv.conf points at 127.0.0.11. The embedded resolver:

  1. Resolves container names on the same network to their current IPs.
  2. Resolves Compose service names (which are container names with a project prefix).
  3. Forwards anything it does not know to the Docker daemon’s configured upstream DNS, usually inherited from the host.

You can override DNS per container:

docker run --dns 1.1.1.1 --dns-search example.com ...

Or daemon-wide in /etc/docker/daemon.json:

{
  "dns": ["8.8.8.8", "1.1.1.1"]
}

A useful debugging trick: docker exec -it <container> getent hosts <name> shows whether name resolution is working without depending on whether ping or curl are installed in the image.

Inspecting and debugging

A small set of commands covers most networking debugging:

docker network ls
docker network inspect app-net
docker inspect <container> | grep -A 20 NetworkSettings
docker port <container>
docker exec -it <container> ip addr
docker exec -it <container> ss -tlnp

If a container cannot reach another:

  1. Are both on the same user-defined network? Check with docker network inspect.
  2. Is the target actually listening on the expected port and interface (0.0.0.0, not 127.0.0.1)?
  3. If publishing, did you forget the host port mapping?
  4. On Linux, did a firewall rule (UFW, firewalld) block the bridge interface?

Wrap up

Docker networking is built around a small number of primitives: bridge networks, port publishing via iptables, and an embedded DNS server. The two habits that prevent most networking pain: always use a user-defined bridge for multi-container setups, and never publish ports that only need to be reached by other containers on the same network.

Once these patterns are second nature, the leap to Docker Compose — which essentially codifies this style — and beyond to Kubernetes networking is much smaller. The cluster details change, but the mental model of “isolated network, names resolve to addresses, publish only what needs to leave” carries over almost directly.