DevOps.Academy
CoursesCurriculumPricing

    Loading…

    ↑ ↓ move · ↵ open · Ctrl K anywhere

    ◔My learningSign in
    DevOps.Academy

    Free, hands-on DevOps lessons. Made with care in India.

    Learn

    All coursesLinux roadmapCurriculumMy learning

    Academy

    PricingCertificates

    Legal

    PrivacyTermsContact

    devops.rajeev.pro

    DockerLesson 10 of 14
    Your progress · 0%
    On this page
    1. Bridge networks
    2. Container DNS
    3. Published ports
    4. Network drivers choose the isolation boundary
    ↖ Course roadmap
    Lesson 10 · Networking

    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.

    30 min read · intermediate · hands-on

    By the end of this lesson you'll be able to

    0 of 5 complete

    Bridge 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 ls
    • dockerbuild and run containers
    • networkmanage networks
    • lslist 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)
    NetworkWhat 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
    hostno namespace: the host's interfaces and ports, no isolation
    noneonly 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 300
    • dockerbuild and run containers
    • runstart a new container
    • -drun in the background
    • --name a1name the container
    • alpine:3.22the image to start
    • sleep 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 a2
    • execrun a command in a running container
    • a1the container
    • ping -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 appnet
    • dockerbuild and run containers
    • networkmanage networks
    • createcreate one
    docker run -d --name api --network appnet price-api:multi
    • runstart a new container
    • -drun in the background
    • --name apiname the container
    • --network appnetwhich network to join
    • price-api:multithe image to start
    docker run -d --name client --network appnet alpine:3.22 sleep 300
    • --name clientname the container
    • alpine:3.22the image to start
    • sleep 300the command run inside, instead of the image’s default
    docker exec client wget -qO- api:8080/health
    • execrun a command in a running container
    • clientthe container
    • wget -qO- api:8080/healththe command to run inside it
    docker exec client cat /etc/resolv.conf
    • cat /etc/resolv.confthe command to run inside it
    docker exec client nslookup api 127.0.0.11 | grep -A1 Name
    • nslookup api 127.0.0.11the command to run inside it
    • |pipe: send output to the next command
    • grepfind 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.

    Names resolve inside a network. Between networks there's neither DNS nor a route until you connect them.© Animation: DevOps Academy

    Names resolve inside a network. Between networks there's neither DNS nor a route until you connect them.© Animation: DevOps Academy

    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/health
    • dockerbuild and run containers
    • execrun a command in a running container
    • a1the container
    • wget -qO- -T 3 api:8080/healththe command to run inside it
    docker exec a1 wget -qO- -T 3 172.18.0.2:8080/health
    • wget -qO- -T 3 172.18.0.2:8080/healththe command to run inside it
    docker network connect appnet a1
    • networkmanage networks
    • connectattach a container to a network
    docker exec a1 wget -qO- api:8080/health
    • wget -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:multi
    • dockerbuild and run containers
    • runstart a new container
    • -drun in the background
    • --name api2name the container
    • --network appnetwhich network to join
    • --network-alias backendan extra DNS name on that network
    • price-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 -2
    • execrun a command in a running container
    • clientthe container
    • nslookup backendthe command to run inside it
    • |pipe: send output to the next command
    • grepfind lines that match a pattern
    • tailshow 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-alpine
    • dockerbuild and run containers
    • runstart a new container
    • -drun in the background
    • --name webname the container
    • -Ppublish every exposed port on a random host port
    • nginx:1.30-alpinethe image to start
    docker port web
    • portshow published ports
    • webthe 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
    FormListens on
    -p 8080:80every host address, IPv4 and IPv6: reachable from the network
    -p 127.0.0.1:8080:80this machine only
    -p 8080:80/udpUDP instead of TCP
    -Prandom 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.

    Published ports can bypass your firewall

    Docker writes its own iptables rules for published ports, and on Linux they're processed before ufw's. ufw deny 5432 won't stop -p 5432:5432 from being reachable from the internet. Bind to 127.0.0.1, filter in the DOCKER-USER chain, or use the cloud provider's firewall, and test from another machine.

    In production

    Put a reverse proxy (nginx, Caddy, Traefik) or a load balancer in front, publish only 80 and 443, and keep databases on an internal network with no published port. Separate stacks get separate networks.

    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.

    Pick the driver by where containers must talk: one host (bridge), the host itself (host), the LAN (macvlan) or many hosts (overlay).© Diagram: DevOps Academy

    Pick the driver by where containers must talk: one host (bridge), the host itself (host), the LAN (macvlan) or many hosts (overlay).© Diagram: DevOps Academy
    $ 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 link
    • dockerbuild and run containers
    • runstart a new container
    • --rmdelete the container when it exits
    • --network nonewhich network to join
    • alpine:3.22the image to start
    • ip linkthe command run inside, instead of the image’s default
    docker network ls --format '{{.Name}} {{.Driver}}'
    • networkmanage networks
    • lslist them
    • --format '{{.Name}} {{.Driver}}'print only these fields (Go template)
    docker inspect --format '{{json .NetworkSettings.Networks}}' api
    • inspectshow every detail as JSON
    • --format '{{json .NetworkSettings.Networks}}'print only these fields (Go template)
    • apithe container or image
    ⌘
    Mini mission

    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.

    Input: Docker and the price-api image from lesson 7Output: wget api:8080/health succeeding from the client, and bad address for db
    Reveal 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 backend
    • dockerbuild and run containers
    • networkmanage networks
    • createcreate one
    • &&run the next command only if this one worked
    docker run -d --name db --network backend -e POSTGRES_PASSWORD=devpass postgres:17-alpine
    • runstart a new container
    • -drun in the background
    • --name dbname the container
    • --network backendwhich network to join
    • -e POSTGRES_PASSWORD=devpassset an environment variable inside
    • postgres:17-alpinethe image to start
    docker run -d --name api --network backend price-api:multi
    • --name apiname the container
    • price-api:multithe image to start
    docker network connect frontend api
    • connectattach 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 join
    • alpine:3.22the image to start
    • sleep 300the command run inside, instead of the image’s default
    docker exec proxy wget -qO- api:8080/health
    • execrun a command in a running container
    • proxythe container
    • wget -qO- api:8080/healththe command to run inside it
    docker exec proxy nslookup db 127.0.0.11 | grep -m1 NXDOMAIN
    • nslookup db 127.0.0.11the command to run inside it
    • |pipe: send output to the next command
    • grepfind lines that match a pattern
    docker inspect api --format '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}'
    • inspectshow every detail as JSON
    • apithe 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 backend
    • rmdelete a container
    • -fstop it first if running
    • dbthe container
    • apithe container
    • rmdelete 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 done

    Run each task in your own account or terminal, then tick it off.

    Knowledge check

    Quick check

    Question 1 of 5

    Two containers on the default bridge network can't reach each other by name. Why?

    Go deeper

    Official reading for this lesson

    • DocsDocker Docs: networking overviewDocker Docs ↗
    • DocsDocker Docs: bridge network driverDocker Docs ↗
    • DocsDocker Docs: port publishing and mappingDocker Docs ↗
    • DocsDocker Docs: packet filtering and firewallsDocker Docs ↗

    Or get every quiz answer right and it completes itself.

    ← PreviousRunning containers well: restart policies, limits, config and ComposeNext lesson →Container security: scanning, non-root, runtime limits and secrets