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.
| Driver | When | Notes |
|---|---|---|
bridge default | docker run with no network flags | containers only see each other by IP, no DNS by name |
bridge user-defined | docker network create or compose | built-in DNS, you reach containers or services by name |
host | --network host | the container shares the host's network namespace. With caveats on macOS and Windows, see below |
none | --network none | no network, only loopback |
overlay | Swarm | a network between nodes of a cluster |
macvlan | rare | the 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:
- First it looks for a container with that name in the networks the container itself belongs to.
- 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.
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:
-p 8080:80listens on all of the host's addresses (0.0.0.0, and on newer Docker versions also[::]). The port is reachable from outside if the host has a public IP. On a dev machine that's usually more than you want.-p 127.0.0.1:8080:80listens only on loopback. Unreachable from outside, a safe choice for local development.- On macOS and Windows, publishing goes through the Docker Desktop VM. The
port is visible from your computer, but
0.0.0.0here refers to the VM, not to your laptop's network interfaces. - The right side has to match the port the process actually listens on. If
you publish
8080:80while the app listens on8080, the connection will reach the container, but nothing inside will accept it. - In compose, the
ports:block sets the same thing.
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:
-
docker network lsanddocker network inspect <network>: which networks exist and who's actually connected to them (theContainerssection). -
docker inspect <container> --format '{{json .NetworkSettings.Networks}}': which networks the container is connected to. -
docker exec <container> cat /etc/resolv.conf: for a network of your own there should benameserver 127.0.0.11. -
docker port <container>: what's published to the host and on which address. -
The image might not have networking tools: distroless has nothing, alpine has
ping,wgetandnslookupfrom busybox, but nocurlordig. 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 shIt sees the same network, the same
localhostand the same names. If you needtcpdumpordig, usenicolaka/netshoot:v0.16instead 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
- Started containers with
docker runwithout--networkand expected them to find each other by name. That never happens on the default bridge. - Reached a neighbor through
localhost. Inside a container,localhostis the container itself, you can only reach a neighbor by name. - Split services across different
networksin compose and forgot that there's neither DNS nor a route between networks. - "Fixed" isolation by putting everything on one network. The connection works now, but the admin panel can see the database.
- Published a port from the image's
EXPOSE, while the app listens on a different one. - Wrote
-p 8080:80on a machine with a public IP and opened the service to the whole internet. For local development, write127.0.0.1:on the left.
What you'll need for the tasks
docker network create,docker network connect,docker network ls,docker network inspectdocker exec <container> wget -qO- -T 3 http://<name>/anddocker exec <container> cat /etc/resolv.conf- the
networks:block on a service and at the top level of a compose file,docker compose up -d,docker compose ps,docker compose logs - the
ports:block in compose,docker port,curl http://127.0.0.1:PORT/
In the next lesson we'll tie it all together with Docker Compose.
Interview-style questions
Interview questions on this topic: 2
Included with practice access, $49 one-time. Get access
Answer out loud or in writing, then reveal the answer and honestly mark whether you knew it.
Done reading?
Mark the lesson as read to move your track progress forward.
Lesson marked as done.
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.
Tell the agent: use MCP izi, give me lesson docker-basics-06-networking
Progress saved in this browser
Clear your history or open this on your phone, and the marks will be gone. Leave your email and they'll move to your account. No password needed.
Done. moved to your account. You can see your progress on any device. My progress