Networking: bridge networks, published ports and container DNS
How containers talk: by name on a user-defined network, to the outside through published ports, and not at all across networks, with Docker's embedded DNS explained.
By the end of this lesson you'll be able to
0 of 5 completeBridge networks
Every container gets its own network namespace (lesson 2): its own interfaces, IP address and ports. A network connects those namespaces. Docker starts with three:
$ docker network ls NETWORK ID NAME DRIVER SCOPE d781217f08f8 bridge bridge local e3398db40859 host host local 6094544119b7 none null local $ docker network inspect bridge --format '{{range .IPAM.Config}}{{.Subnet}} gw {{.Gateway}}{{end}}' 172.17.0.0/16 gw 172.17.0.1
docker network lsdockerbuild and run containersnetworkmanage networkslslist them
docker network inspect bridge --format '{{range .IPAM.Config}}{{.Subnet}} gw {{.Gateway}}{{end}}'inspectshow details as JSON--format '{{range .IPAM.Config}}{{.Subnet}} gw {{.Gateway}}{{end}}'print only these fields (Go template)
| Network | What the container gets |
|---|---|
bridge (default) | an address on 172.17.0.0/16 behind NAT; no DNS by name |
a user-defined bridge (docker network create) | its own subnet, DNS by name, isolation from other networks |
host | no namespace: the host's interfaces and ports, no isolation |
none | only a loopback interface |
The default bridge is kept for old setups. Its biggest gap:
$ docker run -d --name a1 alpine:3.22 sleep 300 $ docker run -d --name a2 alpine:3.22 sleep 300 $ docker exec a1 ping -c1 a2 ping: bad address 'a2'
docker run -d --name a1 alpine:3.22 sleep 300dockerbuild and run containersrunstart a new container-drun in the background--name a1name the containeralpine:3.22the image to startsleep 300the command run inside, instead of the image’s default
docker run -d --name a2 alpine:3.22 sleep 300--name a2name the container
docker exec a1 ping -c1 a2execrun a command in a running containera1the containerping -c1 a2the command to run inside it
Container DNS
Create your own network and names work:
$ docker network create appnet $ docker run -d --name api --network appnet price-api:multi $ docker run -d --name client --network appnet alpine:3.22 sleep 300 $ docker exec client wget -qO- api:8080/health {"status":"ok","version":""} $ docker exec client cat /etc/resolv.conf nameserver 127.0.0.11 options ndots:0 $ docker exec client nslookup api 127.0.0.11 | grep -A1 Name Name: api Address: 172.18.0.2
docker network create appnetdockerbuild and run containersnetworkmanage networkscreatecreate one
docker run -d --name api --network appnet price-api:multirunstart a new container-drun in the background--name apiname the container--network appnetwhich network to joinprice-api:multithe image to start
docker run -d --name client --network appnet alpine:3.22 sleep 300--name clientname the containeralpine:3.22the image to startsleep 300the command run inside, instead of the image’s default
docker exec client wget -qO- api:8080/healthexecrun a command in a running containerclientthe containerwget -qO- api:8080/healththe command to run inside it
docker exec client cat /etc/resolv.confcat /etc/resolv.confthe command to run inside it
docker exec client nslookup api 127.0.0.11 | grep -A1 Namenslookup api 127.0.0.11the command to run inside it|pipe: send output to the next commandgrepfind lines that match a pattern
127.0.0.11 is Docker's embedded DNS server. It answers with the containers on the same network and
forwards everything else (example.com) to the host's DNS. Use names, never container IPs: an IP changes
when the container is recreated, the name doesn't.
Networks are also a security boundary. From a1, on the default bridge, api doesn't exist, and even its
IP address isn't reachable:
$ docker exec a1 wget -qO- -T 3 api:8080/health wget: bad address 'api:8080' $ docker exec a1 wget -qO- -T 3 172.18.0.2:8080/health wget: download timed out $ docker network connect appnet a1 # a second interface, in appnet $ docker exec a1 wget -qO- api:8080/health {"status":"ok","version":""}
docker exec a1 wget -qO- -T 3 api:8080/healthdockerbuild and run containersexecrun a command in a running containera1the containerwget -qO- -T 3 api:8080/healththe command to run inside it
docker exec a1 wget -qO- -T 3 172.18.0.2:8080/healthwget -qO- -T 3 172.18.0.2:8080/healththe command to run inside it
docker network connect appnet a1networkmanage networksconnectattach a container to a network
docker exec a1 wget -qO- api:8080/healthwget -qO- api:8080/healththe command to run inside it
A container can sit on several networks. That's how you split a stack: a frontend network with the proxy and
the API, a backend network with the API and the database. The proxy can't touch the database at all. In
Compose, list networks: per service.
One name, several containers
$ docker run -d --name api2 --network appnet --network-alias backend price-api:multi $ docker run -d --name api3 --network appnet --network-alias backend price-api:multi $ docker exec client nslookup backend | grep Address | tail -2 Address: 172.18.0.6 Address: 172.18.0.5
docker run -d --name api2 --network appnet --network-alias backend price-api:multidockerbuild and run containersrunstart a new container-drun in the background--name api2name the container--network appnetwhich network to join--network-alias backendan extra DNS name on that networkprice-api:multithe image to start
docker run -d --name api3 --network appnet --network-alias backend price-api:multi--name api3name the container
docker exec client nslookup backend | grep Address | tail -2execrun a command in a running containerclientthe containernslookup backendthe command to run inside it|pipe: send output to the next commandgrepfind lines that match a patterntailshow the last lines
DNS returns both addresses, so clients spread across them. It's crude (no health checks, clients cache
answers), but it's what Compose uses when you scale a service: docker compose up -d --scale api=3.
Published ports
Containers on one network reach each other's ports directly. Publishing with -p is only for traffic from
outside, through the host:
$ docker run -d --name web -P nginx:1.30-alpine # -P: every EXPOSEd port, random host ports $ docker port web 80/tcp -> 0.0.0.0:32768 80/tcp -> [::]:32768 $ docker run -d --name web -p 127.0.0.1:8080:80 nginx:1.30-alpine $ docker port web 80/tcp -> 127.0.0.1:8080
docker run -d --name web -P nginx:1.30-alpinedockerbuild and run containersrunstart a new container-drun in the background--name webname the container-Ppublish every exposed port on a random host portnginx:1.30-alpinethe image to start
docker port webportshow published portswebthe container
docker run -d --name web -p 127.0.0.1:8080:80 nginx:1.30-alpine-p 127.0.0.1:8080:80publish a port, host:container
| Form | Listens on |
|---|---|
-p 8080:80 | every host address, IPv4 and IPv6: reachable from the network |
-p 127.0.0.1:8080:80 | this machine only |
-p 8080:80/udp | UDP instead of TCP |
-P | random high ports for every EXPOSE in the image |
In lesson 9, only web published a port. api and db were reachable from inside shop_default and from
nowhere else. Publish the entry point; keep everything behind it unpublished.
Network drivers choose the isolation boundary
bridge is the normal isolated network; host shares the host namespace; none disables networking. macvlan
puts containers on a Layer-2 segment, while overlay connects Swarm nodes. IPv6 must be enabled deliberately.
$ docker run --rm --network none alpine:3.22 ip link $ docker network ls --format '{{.Name}} {{.Driver}}' $ docker inspect --format '{{json .NetworkSettings.Networks}}' api
docker run --rm --network none alpine:3.22 ip linkdockerbuild and run containersrunstart a new container--rmdelete the container when it exits--network nonewhich network to joinalpine:3.22the image to startip linkthe command run inside, instead of the image’s default
docker network ls --format '{{.Name}} {{.Driver}}'networkmanage networkslslist them--format '{{.Name}} {{.Driver}}'print only these fields (Go template)
docker inspect --format '{{json .NetworkSettings.Networks}}' apiinspectshow every detail as JSON--format '{{json .NetworkSettings.Networks}}'print only these fields (Go template)apithe container or image
Split a stack into two networks
Create a frontend and a backend network. Put a proxy-like client on frontend, the API on both, and a database container on backend only. Prove the client can reach the API by name but can't resolve or reach the database.
wget api:8080/health succeeding from the client, and bad address for dbReveal one safe solution
$ docker network create frontend && docker network create backend $ docker run -d --name db --network backend -e POSTGRES_PASSWORD=devpass postgres:17-alpine $ docker run -d --name api --network backend price-api:multi $ docker network connect frontend api $ docker run -d --name proxy --network frontend alpine:3.22 sleep 300 $ docker exec proxy wget -qO- api:8080/health {"status":"ok","version":""} $ docker exec proxy nslookup db 127.0.0.11 | grep -m1 NXDOMAIN ** server can't find db: NXDOMAIN $ docker inspect api --format '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}' backend frontend $ docker rm -f db api proxy && docker network rm frontend backend
docker network create frontend && docker network create backenddockerbuild and run containersnetworkmanage networkscreatecreate one&&run the next command only if this one worked
docker run -d --name db --network backend -e POSTGRES_PASSWORD=devpass postgres:17-alpinerunstart a new container-drun in the background--name dbname the container--network backendwhich network to join-e POSTGRES_PASSWORD=devpassset an environment variable insidepostgres:17-alpinethe image to start
docker run -d --name api --network backend price-api:multi--name apiname the containerprice-api:multithe image to start
docker network connect frontend apiconnectattach a container to a network
docker run -d --name proxy --network frontend alpine:3.22 sleep 300--name proxyname the container--network frontendwhich network to joinalpine:3.22the image to startsleep 300the command run inside, instead of the image’s default
docker exec proxy wget -qO- api:8080/healthexecrun a command in a running containerproxythe containerwget -qO- api:8080/healththe command to run inside it
docker exec proxy nslookup db 127.0.0.11 | grep -m1 NXDOMAINnslookup db 127.0.0.11the command to run inside it|pipe: send output to the next commandgrepfind lines that match a pattern
docker inspect api --format '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}'inspectshow every detail as JSONapithe container or image--format '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}'print only these fields (Go template)
docker rm -f db api proxy && docker network rm frontend backendrmdelete a container-fstop it first if runningdbthe containerapithe containerrmdelete one
The API is the only container on both networks, so it's the only path to the database. If the proxy is ever compromised, the database isn't even a name it can look up.
Try it yourself
0 of 6 steps doneRun each task in your own account or terminal, then tick it off.
Quick check
Two containers on the default bridge network can't reach each other by name. Why?
Official reading for this lesson
Or get every quiz answer right and it completes itself.