Skip to main content
Docker · lesson 4 of 9
Docker intermediate Lesson 4 of 9

Also part of: Networking

Docker Networking

Connect containers by name on user-defined bridge networks, see network isolation, aliases, the host and none drivers, and reach the host from a container.

By SkillByExample · Updated

Every container gets its own network namespace: its own interfaces, IP address and localhost. Docker networks decide which containers can reach each other. The examples use nginx:1.27-alpine as a server and throwaway alpine:3.20 containers as clients.

The default bridge has no DNS

docker network ls
docker run -d --name webserver nginx:1.27-alpine
docker run --rm alpine:3.20 wget -qO- -T 2 http://webserver
$ docker network ls
NETWORK ID     NAME      DRIVER    SCOPE
fb23f7f3aedd   bridge    bridge    local
012040dcda2c   host      host      local
a6f8ee7a854a   none      null      local
$ docker run -d --name webserver nginx:1.27-alpine
0ee8e5b10241465ffdae7974e0c9ed554e65b64464691758f8c092c0cd9ae861
$ docker run --rm alpine:3.20 wget -qO- -T 2 http://webserver
wget: bad address 'webserver'

Docker starts with three networks, one per built-in driver. Containers without --network join the default bridge, and on that network container names do not resolve — the client could only reach webserver by IP address, which changes every time the container is recreated.

User-defined networks resolve names

docker network create appnet
docker network connect appnet webserver
docker run --rm --network appnet alpine:3.20 wget -qO- http://webserver | grep -o '<title>.*</title>'
docker inspect --format '{{range $net, $cfg := .NetworkSettings.Networks}}{{$net}}={{$cfg.IPAddress}} {{end}}' webserver
$ docker network create appnet
a198d2577a55c1fba12b12ce02824659c5c77775d8c2923813b4b81f3031810f
$ docker network connect appnet webserver
$ docker run --rm --network appnet alpine:3.20 wget -qO- http://webserver | grep -o '<title>.*</title>'
<title>Welcome to nginx!</title>
$ docker inspect --format '{{range $net, $cfg := .NetworkSettings.Networks}}{{$net}}={{$cfg.IPAddress}} {{end}}' webserver
appnet=172.18.0.2 bridge=172.17.0.2

A network you create with docker network create (bridge driver by default) runs Docker’s embedded DNS server at 127.0.0.11, so webserver resolves to its address on that network. docker network connect attached the running container to a second network; it now has one interface and IP per network. Normally you just pass --network appnet to docker run.

Networks isolate containers

docker network create othernet
docker run --rm --network othernet alpine:3.20 wget -qO- -T 2 http://webserver
docker run --rm --network othernet alpine:3.20 wget -qO- -T 2 http://172.18.0.2
docker network rm othernet
$ docker network create othernet
cba06b6d02ca4ca80f1d588ed3b4e18a34a09094317f47b915909b1a9f0745e0
$ docker run --rm --network othernet alpine:3.20 wget -qO- -T 2 http://webserver
wget: bad address 'webserver'
$ docker run --rm --network othernet alpine:3.20 wget -qO- -T 2 http://172.18.0.2
wget: download timed out
$ docker network rm othernet
othernet

A container on othernet can neither resolve webserver nor reach its appnet IP address — Docker drops traffic between bridge networks. This is how you keep, say, a database reachable from the API but not from the public-facing proxy: put each tier on its own network and attach only the containers that must bridge them to both.

Network aliases

docker run -d --name pg-main --network appnet --network-alias db alpine:3.20 sleep 300
docker run --rm --network appnet alpine:3.20 sh -c 'ping -c1 -W1 db | head -2'
docker rm -f pg-main
$ docker run -d --name pg-main --network appnet --network-alias db alpine:3.20 sleep 300
77755033e2725e0a9738fcd3cb970078228665766efe2058ba3e829f696ed8ec
$ docker run --rm --network appnet alpine:3.20 sh -c 'ping -c1 -W1 db | head -2'
PING db (172.18.0.3): 56 data bytes
64 bytes from 172.18.0.3: seq=0 ttl=64 time=1.071 ms
$ docker rm -f pg-main
pg-main

--network-alias adds an extra DNS name on that network. Clients can be configured with a stable role name like db while the actual container (pg-main) is replaced or renamed. Docker Compose uses the same mechanism to make every service reachable by its service name.

The none and host drivers

docker run --rm --network none alpine:3.20 ip -o -4 addr
docker run --rm --network appnet alpine:3.20 ip -o -4 addr
docker run --rm --network host alpine:3.20 ip -o -4 addr
$ docker run --rm --network none alpine:3.20 ip -o -4 addr
1: lo    inet 127.0.0.1/8 scope host lo\       valid_lft forever preferred_lft forever
$ docker run --rm --network appnet alpine:3.20 ip -o -4 addr
1: lo    inet 127.0.0.1/8 scope host lo\       valid_lft forever preferred_lft forever
2: eth0    inet 172.18.0.3/16 brd 172.18.255.255 scope global eth0\       valid_lft forever preferred_lft forever
$ docker run --rm --network host alpine:3.20 ip -o -4 addr
1: lo    inet 127.0.0.1/8 scope host lo\       valid_lft forever preferred_lft forever
4: eth0    inet 192.0.2.2/24 brd 192.0.2.255 scope global eth0\       valid_lft forever preferred_lft forever
5: docker0    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0\       valid_lft forever preferred_lft forever
186: br-a198d2577a55    inet 172.18.0.1/16 brd 172.18.255.255 scope global br-a198d2577a55\       valid_lft forever preferred_lft forever

none leaves only loopback — no way in or out, for jobs that must not touch the network. A bridge container gets one virtual eth0 on its network. host skips the network namespace entirely: the container sees the host’s interfaces, including docker0 (the default bridge) and br-a198d2577a55 (the bridge behind appnet). A server in a host-network container binds host ports directly, with no -p needed — and no isolation. On Docker Desktop, “host” is the Linux VM, not your Mac or PC.

localhost is the container, not the host

A small web server is running on the host’s port 9000 (python3 -m http.server 9000):

curl -s localhost:9000
docker run --rm alpine:3.20 wget -qO- -T 2 http://localhost:9000
docker run --rm --add-host host.docker.internal:host-gateway alpine:3.20 wget -qO- -T 2 http://host.docker.internal:9000
$ curl -s localhost:9000
hello from the host
$ docker run --rm alpine:3.20 wget -qO- -T 2 http://localhost:9000
wget: can't connect to remote host (127.0.0.1): Connection refused
$ docker run --rm --add-host host.docker.internal:host-gateway alpine:3.20 wget -qO- -T 2 http://host.docker.internal:9000
hello from the host

Inside the container, 127.0.0.1 is the container’s own loopback, where nothing listens. The special value host-gateway makes --add-host map a name to the host’s address on the bridge (172.17.0.1 here). Docker Desktop defines host.docker.internal for you; on Linux you add it yourself as shown. The host service must listen on more than 127.0.0.1 for this to work.

Publish on localhost only

docker run -d --name local-only -p 127.0.0.1:8082:80 nginx:1.27-alpine
docker port local-only
docker rm -f local-only
$ docker run -d --name local-only -p 127.0.0.1:8082:80 nginx:1.27-alpine
90873a678ce05c286afef03f7f865be07dec6e6f7ab8085487f1f374a25a4e94
$ docker port local-only
80/tcp -> 127.0.0.1:8082
$ docker rm -f local-only
local-only

A plain -p 8082:80 binds 0.0.0.0, exposing the port on every host interface — and Docker’s firewall rules can bypass a host firewall such as ufw. Prefixing the host IP limits the port to connections from the host itself, the right default for databases and admin UIs on a dev machine.

Practice

  1. Print the subnet and gateway of appnet with a --format template, then disconnect webserver from it and remove both the container and the network.
Solution
docker network inspect --format '{{range .IPAM.Config}}{{.Subnet}} gw {{.Gateway}}{{end}}' appnet
docker network disconnect appnet webserver
docker rm -f webserver
docker network rm appnet
$ docker network inspect --format '{{range .IPAM.Config}}{{.Subnet}} gw {{.Gateway}}{{end}}' appnet
172.18.0.0/16 gw 172.18.0.1
$ docker network disconnect appnet webserver
$ docker rm -f webserver
webserver
$ docker network rm appnet
appnet

The gateway (.1) is the host’s side of the bridge. A network with containers still attached cannot be removed.

  1. Create a network shop and run nginx on it as catalog with two aliases, products and inventory. Fetch the page through all three names from one client container.
Solution
docker network create shop
docker run -d --name catalog --network shop --network-alias products --network-alias inventory nginx:1.27-alpine
docker run --rm --network shop alpine:3.20 sh -c 'for h in catalog products inventory; do wget -qO- http://$h | grep -o "<title>.*</title>"; done'
docker rm -f catalog
docker network rm shop
$ docker network create shop
da00fb7fa2419dcd7b5672f8ddbff4213bf7c4571f24c0aefc78b3e0db12c7c4
$ docker run -d --name catalog --network shop --network-alias products --network-alias inventory nginx:1.27-alpine
842423b977b4209071e13e371200ad6f598ef3892100be8b94d792a12cb48ce3
$ docker run --rm --network shop alpine:3.20 sh -c 'for h in catalog products inventory; do wget -qO- http://$h | grep -o "<title>.*</title>"; done'
<title>Welcome to nginx!</title>
<title>Welcome to nginx!</title>
<title>Welcome to nginx!</title>
$ docker rm -f catalog
catalog
$ docker network rm shop
shop
  1. Create a network lab with the subnet 10.10.0.0/24 and start a container on it with the fixed address 10.10.0.50.
Solution
docker network create --subnet 10.10.0.0/24 lab
docker run --rm --network lab --ip 10.10.0.50 alpine:3.20 ip -o -4 addr show eth0
docker network rm lab
$ docker network create --subnet 10.10.0.0/24 lab
3d509c982991d20866f7b4a97901263fa4964e39e4119816338aa30b4ee272e8
$ docker run --rm --network lab --ip 10.10.0.50 alpine:3.20 ip -o -4 addr show eth0
2: eth0    inet 10.10.0.50/24 brd 10.10.0.255 scope global eth0\       valid_lft forever preferred_lft forever
$ docker network rm lab
lab

--ip only works on user-defined networks with an explicit --subnet. Prefer names over fixed IPs; use this when something outside Docker needs a predictable address.

Frequently Asked Questions

How do containers talk to each other?
Put them on the same user-defined bridge network with docker network create and --network. Docker runs an embedded DNS server on such networks, so each container can reach the others by container name or network alias. The default bridge network has no name resolution.
What is the difference between bridge, host, and none networks?
bridge gives each container its own network namespace with a private IP behind NAT, and ports must be published with -p to be reachable from outside. host removes network isolation, so the container uses the host's interfaces directly. none gives the container only a loopback interface.
Why can't I reach localhost from inside a container?
Inside a container, localhost is the container's own loopback interface, not the host's. To reach a service on the host, use host.docker.internal: Docker Desktop defines it automatically, and on Linux you add it with --add-host host.docker.internal:host-gateway.
Are containers on different Docker networks isolated?
Yes. Containers on different user-defined networks cannot resolve each other's names and cannot reach each other's IP addresses. Attach a container to both networks with docker network connect when it needs to talk to both sides.