Sign in
← Docker
14 min · medium · Lesson 6 of 8

Docker networking

Default bridge vs user-defined, DNS by service name, host and none, port publishing. Why two different networks can't see each other.

Why

Almost any containerized app is made of several parts: a web server, a database, a cache. They need to find each other, and you need to open some of them up from the host. If you don't understand how Docker hands out networks, you get the classics: "it works on my machine, but in compose the service can't see the database" or "the port is open, but curl stays silent."

In this lesson we'll go through network drivers, Docker's built-in DNS, isolation between networks, and publishing ports.

Network drivers

A network in Docker is created by a driver. There are several, but in everyday work you'll mostly run into the first four rows of the table.

DriverWhenNotes
bridge defaultdocker run with no network flagscontainers only see each other by IP, no DNS by name
bridge user-defineddocker network create or composebuilt-in DNS, you reach containers or services by name
host--network hostthe container shares the host's network namespace. With caveats on macOS and Windows, see below
none--network noneno network, only loopback
overlaySwarma network between nodes of a cluster
macvlanrarethe container appears on the local network as a separate host with its own MAC

Most of the time you need a bridge of your own. Compose creates one for every project automatically. Let's talk separately about the default bridge and about host: those are the two that cause the most confusion.

Default bridge: names don't work

When you run docker run with no network flags, the container lands on the bridge network. Docker creates it at install time. It has one catch: containers don't find each other by name.

docker run -d --name db postgres:16.4-alpine
docker run -d --name app alpine:3.20.3 sleep 3600   # both on the default bridge
docker exec app ping -c1 db                          # ping: bad address 'db'

app doesn't know about db. The only way to reach it is by IP, and a container's IP changes when it's recreated. Nobody is going to hardcode that into a config. So the default bridge is almost never what you want.

Inside such a container, /etc/resolv.conf has the host's DNS server, not Docker's resolver. On Docker Desktop that's the address of the service VM, for example nameserver 192.168.65.7.

A network of your own: names work

docker network create app-net
docker run -d --network app-net --name db postgres:16.4-alpine
docker run -d --network app-net --name app alpine:3.20.3 sleep 3600
docker exec app ping -c1 db
PING db (192.168.16.2): 56 data bytes
64 bytes from 192.168.16.2: seq=0 ttl=64 time=0.112 ms

db resolves to the container's IP (yours will be different). This is Docker's built-in DNS at work. Look inside the container:

docker exec app cat /etc/resolv.conf
nameserver 127.0.0.11
options ndots:0

127.0.0.11 is Docker's internal resolver. Every container connected to a user-defined network has it. The order is:

  1. First it looks for a container with that name in the networks the container itself belongs to.
  2. If it doesn't find one, it forwards the request to the host's DNS server.

That's why db resolves locally, while google.com goes out to the internet.

You can connect an already-running container to a user-defined network without recreating it: docker network connect <network> <container>. You can disconnect it with docker network disconnect.

On docker compose up, compose creates a bridge of its own for the project, named <project>_default. That's why in compose files services reach each other by service name with no extra setup.

The trap: different networks can't see each other

A common cause of a "Name does not resolve" error or a timeout in compose: the services sit in different networks blocks. Docker only resolves names within shared networks. Between networks there's neither DNS nor a route.

Bad:

services:
  web:
    image: shop-api:1.4.2
    networks: [frontend]
  db:
    image: postgres:16.4-alpine
    networks: [backend]
networks:
  frontend:
  backend:

web and db are on different networks. web won't find db by name and will fail on the first connection to the database.

The fix: put them on a shared network.

services:
  web:
    image: shop-api:1.4.2
    networks: [app-net]
  db:
    image: postgres:16.4-alpine
    networks: [app-net]
networks:
  app-net:

If you need isolated networks (the admin panel kept separate from the database), a service can be added to several networks at once:

services:
  web:
    networks: [public-net, db-net]
  db:
    networks: [db-net]
  admin:
    networks: [public-net]

Then web sees both db and admin. And admin only sees web: it can't reach the database directly.

shared network app-net web db name db resolves two separate networks frontend web backend db name not found localhost inside a container web curl localhost:5432 → 127.0.0.1 is web itself → no postgres there → connection refused

host: no network isolation

docker run --network host nginx:1.27.3-alpine

The container shares the host's network namespace. It doesn't need -p, it listens on the host's ports directly. There's no network isolation. You need this rarely: for monitoring agents that need direct access to network interfaces, or for services where every fraction of a millisecond of latency matters.

On Linux, the host is literally your machine. On macOS and Windows, Docker Desktop runs a Linux VM, and host by default means that VM's network: such a container's ports aren't visible from your laptop. Starting with Docker Desktop 4.34 you can enable host networking there too: sign in to your Docker account, then go to Settings → Resources → Network → Enable host networking.

There's no full parity with Linux. Docker Desktop only forwards TCP and UDP, lower-level protocols don't work. A process in the container can't listen on the host's IP addresses, because it doesn't see its interfaces. If an app needs the machine's real network cards (monitoring, broadcast discovery), that only works on Linux.

none: no network

docker run --rm --network none alpine:3.20.3 ping -c1 1.1.1.1
PING 1.1.1.1 (1.1.1.1): 56 data bytes
ping: sendto: Network unreachable

The container is left with only loopback. It can't reach the internet, and nobody can reach it. Good for one-off tasks that shouldn't send anything out.

Publishing ports: -p host:container

To reach a container from the host (or from the internet, if the host faces outward), you need to publish the port:

docker run -d --name web -p 8080:80 nginx:1.27.3-alpine
docker port web
80/tcp -> 0.0.0.0:8080

Now curl http://localhost:8080 lands in the container on port 80. In -p, the left side is the host's port, the right side is the port the process inside the container listens on.

What matters:

services:
  web:
    image: nginx:1.27.3-alpine
    ports:
      - "127.0.0.1:8080:80"   # local only
      - "443:443"             # also from outside

EXPOSE doesn't publish a port

The temptation: "the Dockerfile says EXPOSE 80, so the port is open." No. EXPOSE is image metadata. Without -p, a container from an image with EXPOSE 80 publishes nothing: docker port returns empty output.

EXPOSE has two uses. First: documentation for whoever runs the image. Second: the docker run -P flag (capital P), which publishes all EXPOSE'd ports to random host ports:

docker run -d --name web2 -P nginx:1.27.3-alpine
docker port web2
80/tcp -> 0.0.0.0:32790

In practice -P is rarely used. And remember that EXPOSE knows nothing about which port the app actually listens on: it's just a string the image's author wrote.

Diagnosing when it doesn't work

A checklist for connection refused or a name that won't resolve:

  1. docker network ls and docker network inspect <network>: which networks exist and who's actually connected to them (the Containers section).

  2. docker inspect <container> --format '{{json .NetworkSettings.Networks}}': which networks the container is connected to.

  3. docker exec <container> cat /etc/resolv.conf: for a network of your own there should be nameserver 127.0.0.11.

  4. docker port <container>: what's published to the host and on which address.

  5. The image might not have networking tools: distroless has nothing, alpine has ping, wget and nslookup from busybox, but no curl or dig. Then start a temporary container next to it, in the problem container's network namespace:

    docker run --rm -it --network container:<container> busybox:1.36.1 sh

    It sees the same network, the same localhost and the same names. If you need tcpdump or dig, use nicolaka/netshoot:v0.16 instead of busybox.

Order of diagnosis: first inspect (is it the right network), then DNS (does the name resolve), then the port (is the process listening on what's published), and only then tcpdump (does the packet actually arrive).

Common mistakes

What you'll need for the tasks

In the next lesson we'll tie it all together with Docker Compose.

Done reading?

Mark the lesson as read to move your track progress forward.

Go through it with an AI agent

The agent explains anything unclear in plain words, gives you 3 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.

Get access for $49

Tell the agent: use MCP izi, give me lesson docker-basics-06-networking