Container Networking

The container network model, why the default bridge has no DNS but a user-defined one does, what each native driver is actually for, and how to trace a packet from the host to a container port.

intermediate 24 min lesson hands-on task included

Each container gets its own network namespace (lesson 4), which means its own interfaces, routing table and firewall rules. Docker’s networking layer is what connects those isolated stacks to each other and to the outside world.

Most container networking confusion comes down to one fact that nobody tells beginners: the default bridge network behaves differently from every network you create yourself.


Topic 1: The Container Network Model

Docker networking follows a specification called the Container Network Model (CNM), implemented by an open-source library called libnetwork. Three concepts:

ConceptWhat it is
SandboxA container’s whole network stack: interfaces, routing table, DNS settings
EndpointA network interface that joins a sandbox to a network — one veth end
NetworkA collection of endpoints with connectivity between them

A container can have several endpoints and therefore sit on several networks at once. That is how you build a service that talks to both a public-facing network and a private database network without the two being connected.

libnetwork exposes two pluggable interfaces: network drivers (how connectivity is provided) and IPAM drivers (how addresses are allocated). Third-party drivers plug in at the same points — this is the extensibility story, and it is why Weave, Calico and cloud-native plugins can exist.


Topic 2: The Native Drivers

docker network ls
# NETWORK ID     NAME      DRIVER    SCOPE
# a1b2c3d4e5f6   bridge    bridge    local
# f6e5d4c3b2a1   host      host      local
# 0011223344ff   none      null      local
DriverWhat it doesUse it when
bridgeThe default. A private virtual network on one host, NAT to the outsideAlmost always, on a single host
hostThe container shares the host’s network stack entirelyPerformance-critical or port-scanning tooling
noneNo interfaces at all except loopbackBatch jobs that must not reach the network
overlayA virtual network spanning multiple hostsSwarm services, multi-host clusters
macvlanGives the container its own MAC address on the physical LANLegacy apps that need to look like a physical host
ipvlanLike macvlan, sharing the host’s MACEnvironments where the switch limits MACs per port

host deserves a specific warning: there is no port mapping and no isolation. A container on the host network binding port 80 is the host binding port 80, and it will collide with anything else that wants it.

macvlan is the answer when a container must appear on the physical network with its own IP — some appliances and licence servers genuinely require this — and it needs the physical switch to permit promiscuous mode, which is often where the plan dies.


Topic 3: What Installing Docker Did to Your Host

Before Docker, ip addr on a plain host shows lo and eth0. After installing Docker, there is a new interface:

ip addr show docker0
# 3: docker0: <BROADCAST,MULTICAST,UP> mtu 1500
#     inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0

docker0 is a software bridge — a virtual switch. When you start a container:

  1. Docker creates a veth pair — a virtual cable with two ends.
  2. One end goes into the container’s namespace and is renamed eth0.
  3. The other end stays on the host (you will see it as something like vethf5e485a) and is plugged into docker0.
  4. The container gets an IP from the bridge’s subnet (172.17.0.0/16 by default) and a MAC address.
docker run -d --name web nginx:alpine
docker inspect web --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'
ip addr | grep veth              # the host end of the cable

Outbound traffic from the container is masqueraded (SNAT) to the host’s IP, which is why containers can reach the internet without any configuration. Inbound traffic reaches nothing until you publish a port.


Topic 4: The Default Bridge Has No DNS — and That Is the Whole Lesson

Start two containers with no --network flag and they land on the default bridge:

docker run -d --name dc1 alpine sleep 1d
docker run -d --name dc2 alpine sleep 1d

docker inspect dc2 --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'
# 172.17.0.4

docker exec dc1 ping -c 2 172.17.0.4     # works
docker exec dc1 ping -c 2 dc2            # FAILS: bad address 'dc2'

IP works. Names do not. Now create your own bridge:

docker network create --driver bridge --subnet 10.10.0.0/24 mybridge

docker run -d --name c1 --network mybridge alpine sleep 1d
docker run -d --name c2 --network mybridge alpine sleep 1d

docker exec c1 ping -c 2 c2              # works — resolved by name

User-defined bridge networks have automatic DNS resolution by container name. The default bridge does not. Docker runs an embedded DNS resolver at 127.0.0.11 inside each container on a user-defined network, and it resolves container names and network aliases.

This is not a minor convenience. Container IPs change on every recreate, so anything hard-coding an IP is broken by design. Name resolution is what makes DATABASE_HOST=db work — and it only works if you created the network yourself. Docker Compose (lesson 11) creates a user-defined network for every project automatically, which is why this problem is invisible to people who start with Compose.

User-defined networks also give you better isolation (containers on different user-defined networks cannot reach each other) and live attach/detach:

docker network create backend
docker network connect backend api          # api is now on two networks
docker network disconnect bridge api
docker network inspect mybridge             # lists subnet, gateway and every attached container

Topic 5: Publishing Ports, and What Actually Happens

docker run -d -p 8080:80 --name web nginx:alpine

Under the hood Docker inserts a DNAT rule into the host’s iptables nat table:

sudo iptables -t nat -L DOCKER -n
# DNAT  tcp -- 0.0.0.0/0  0.0.0.0/0  tcp dpt:8080 to:172.17.0.2:80

Traffic arriving on host port 8080 is rewritten to the container’s IP on port 80 and forwarded across the bridge. That is the entire mechanism.

Two operational consequences that surprise people:

Docker’s rules can bypass your firewall. Docker writes into the DOCKER chain, which is traversed before many INPUT-based firewall rules. A ufw-blocked port can still be reachable if a container published it. Bind to a specific interface when you do not want the world reaching it:

docker run -d -p 127.0.0.1:8080:80 nginx:alpine     # localhost only

Containers on the same network do not need publishing at all. If api and db are on backend, api reaches db:5432 directly. Publishing 5432 to the host as well means you have also exposed your database to everything that can reach the host. Publish only what genuinely needs to come in from outside.

docker port web                  # 80/tcp -> 0.0.0.0:8080

Topic 6: Beyond One Host

Everything above stops at the edge of one machine. Containers on host A cannot reach containers on host B by container name, and docker0 is local to each host.

The overlay driver solves this: it builds a virtual layer-2 network across multiple hosts using VXLAN encapsulation, so containers on different machines share one address space and one DNS namespace. Overlay networks are created and managed by Docker Swarm, Docker’s own clustering mode.

Swarm is worth knowing about even though few new deployments choose it:

  • A manager node accepts your desired state (a service) and dispatches units of work (tasks) to workers. Multiple managers are supported; one is the leader.
  • A worker node runs the tasks and reports status back.
  • docker swarm init and docker swarm join set it up; docker service create runs a replicated service across the cluster.

Once you are running containers on multiple hosts, the requirements change shape entirely: you need to survive node failure, scale services, do zero-downtime deployments, and keep it all simple enough to operate. That is the orchestration problem, and it is where Kubernetes — the subject of its own path on this site — took over from Swarm in practice.

Try it yourself: Create two user-defined networks, frontend and backend. Put an nginx container on frontend, a Postgres on backend, and an API container on both. Prove that nginx cannot reach Postgres, but the API can reach both. That three-container layout is the smallest realistic example of network segmentation, and it maps almost exactly onto what a NetworkPolicy does in Kubernetes.

Common mistake: Hard-coding a container’s IP address in configuration. Container IPs are assigned from a pool at start time and change whenever the container is recreated. Put containers on a user-defined network and use container names — that is what the embedded DNS is for.