7. Docker Networking
Almost every real application is more than one process: a web server talks to an API, the API talks to a database, and all of them talk to the outside world. This chapter explains how Docker builds virtual networks out of Linux primitives (namespaces, bridges, virtual Ethernet pairs, and firewall rules), how each network driver trades isolation for reach and performance, and how containers find and talk to each other by name. Understanding these mechanics turns "it can't connect" from guesswork into a systematic diagnosis.
<a id="71-networking-fundamentals"></a>
7.1 Networking Fundamentals
This section covers the model and kernel machinery behind every Docker network, from the abstract building blocks down to the packets rewritten by the firewall.
Container Network Model
Why a model is needed. Docker has to support very different kinds of networking: a private switch on one laptop, a virtual network spanning a hundred servers, containers that appear as physical machines on the office LAN, and third-party software-defined networks. If each one were implemented ad hoc, the CLI, the API, and the container lifecycle would need special cases for every type. The Container Network Model (CNM) is the abstraction that solves this. It describes networking with a few generic concepts, and libnetwork (the networking library inside dockerd) implements it with pluggable drivers.
The three building blocks
| Concept | What it is | Typical Linux implementation (bridge driver) |
|---|---|---|
| Sandbox | The isolated network stack of a container: interfaces, routing table, DNS settings | A network namespace |
| Endpoint | A virtual network interface that joins a sandbox to a network; it has an IP and MAC address | One end of a veth pair (eth0 in the container) |
| Network | A group of endpoints that can talk to each other directly | A Linux bridge (docker0, br-xxxx) or a VXLAN segment |
Two kinds of drivers plug into this model:
- Network drivers create the actual networks and endpoints (
bridge,overlay,macvlan, ...). - IPAM drivers (IP Address Management) hand out subnets and addresses. The built-in
defaultIPAM driver is used unless you choose another one.
Analogy. Think of an office building. Each office is a sandbox: a private room with its own furniture (routing table, DNS settings). A wall jack with a cable is an endpoint. A switch on a floor is a network: every office plugged into it can talk to the others. An office can have several jacks connected to different switches, which is exactly how a container joins multiple networks.
+------------------- Sandbox (container A) --+ +-- Sandbox (container B) --+
| routes, /etc/resolv.conf, iptables | | |
| [Endpoint eth0] [Endpoint eth1] | | [Endpoint eth0] |
+--------|-----------------------|-----------+ +--------|------------------+
| | |
=========+=======================|========================+===== Network "frontend"
|
=================================+============================== Network "backend"
CNM vs CNI. Kubernetes does not use CNM. It uses the Container Network Interface (CNI), a simpler specification where the runtime calls an executable plugin to add or remove a container from a network.
| CNM (Docker) | CNI (Kubernetes, Podman, containerd) | |
|---|---|---|
| Owner | Docker / libnetwork | CNCF |
| Model | Long-lived driver inside the daemon | Plugin binary invoked per operation with JSON config |
| Unit of networking | Container (sandbox) | Pod (one namespace shared by several containers) |
| Typical plugins | bridge, overlay, macvlan | Calico, Cilium, Flannel |
Example: finding the CNM objects of a running container
docker network create appnet
docker run -d --name web --network appnet nginx:1.27-alpine
# The sandbox: a network namespace file managed by Docker
docker inspect -f '{{.NetworkSettings.SandboxKey}}' web
# /var/run/docker/netns/6f1c2a9b8e3d
# The endpoint: one per (container, network) pair
docker inspect -f '{{json .NetworkSettings.Networks.appnet}}' web | jq '{EndpointID, IPAddress, MacAddress}'
# {
# "EndpointID": "a41c...e9",
# "IPAddress": "172.18.0.2",
# "MacAddress": "02:42:ac:12:00:02" # format varies by Engine version
# }
# The network, with its driver and IPAM configuration
docker network inspect appnet -f '{{.Driver}} {{json .IPAM}}'
# bridge {"Driver":"default","Options":{},"Config":[{"Subnet":"172.18.0.0/16","Gateway":"172.18.0.1"}]}
# Enter the sandbox with host tools (useful when the image has no "ip" binary)
sudo nsenter --net="$(docker inspect -f '{{.NetworkSettings.SandboxKey}}' web)" ip addr
docker rm -f web && docker network rm appnet
Key takeaways
- CNM describes networking with three concepts: sandbox (network namespace), endpoint (interface), network (group of connected endpoints).
- libnetwork implements CNM with pluggable network drivers and IPAM drivers.
- A container has exactly one sandbox but can have many endpoints, one per network it joins.
- Kubernetes uses CNI instead of CNM; the ideas are similar but the plugin interface differs.
🧪 Practice
- (Beginner) Create a container attached to two networks and use
docker inspectto list its endpoints, showing the endpoint ID, IP address, and network name of each.- (Intermediate) Use
nsenterwith the container'sSandboxKeyto list its interfaces and routing table from the host, and compare the result with whatdocker exec <container> ip addrreports.- Interview question: "Kubernetes runs containers with containerd, not Docker. Why couldn't it simply reuse Docker's network drivers?" Hint: compare who owns the network lifecycle in CNM (a daemon) versus CNI (the orchestrator calling plugin binaries), and the pod-level shared namespace.
Network Drivers Overview
Why several drivers. There is no single best network. A developer laptop needs isolation and convenience, a high-throughput monitoring agent needs raw host performance, a multi-host cluster needs a network that spans machines, and a legacy application might need to appear on the physical LAN with its own IP address. Each network driver is a different answer to the question "where does this container's traffic go, and who can reach it?"
The built-in drivers
| Driver | Scope | How it works | Typical use |
|---|---|---|---|
bridge | Single host | Private subnet on a Linux bridge, NAT to the outside | Default; most single-host applications |
host | Single host | No separate namespace; uses the host's network stack | Maximum performance, many dynamic ports |
none (null) | Single host | Loopback only | Fully isolated jobs |
overlay | Multi-host (Swarm) | VXLAN tunnels between hosts | Swarm services, cross-host containers |
macvlan | Single host, LAN-visible | Each container gets its own MAC address on a physical interface | Apps that must look like physical devices on the LAN |
ipvlan | Single host, LAN-visible | Containers share the parent's MAC address, each with its own IP | Like macvlan where extra MACs are forbidden; L3 routing setups |
| Plugins | Varies | Third-party drivers installed with docker plugin install | Vendor SDNs, special requirements |
There is also a network mode that is not a driver: --network container:<name> makes a container join another container's sandbox. Both share the same interfaces, IP address, and localhost, which is how sidecars and debugging tools attach to a running container (Kubernetes pods work the same way).
Choosing a driver
Need containers on several hosts to talk directly?
yes --> overlay (Swarm) or an orchestrator's CNI
no --> Must containers appear as devices on the physical LAN?
yes --> macvlan (or ipvlan if the switch/cloud forbids extra MACs)
no --> Need zero network isolation / max performance on Linux?
yes --> host
no --> Need no network at all?
yes --> none
no --> user-defined bridge <-- the usual answer
Example
# Drivers available in this Engine
docker info --format '{{.Plugins.Network}}'
# [bridge host ipvlan macvlan null overlay]
# Networks that exist on every installation
docker network ls
# NETWORK ID NAME DRIVER SCOPE
# 3c5e1f0a9b2d bridge bridge local
# 8a7d6c5b4e3f host host local
# 1f2e3d4c5b6a none null local
# Only one "host" and one "none" network can exist; you can create many bridges
docker network create -d bridge team-a
docker network create -d bridge team-b
docker network ls --filter driver=bridge --format '{{.Name}}'
# Share another container's network namespace
docker run -d --name web nginx:1.27-alpine
docker run --rm --network container:web alpine wget -qO- http://localhost | head -n 4
# <!DOCTYPE html> ... nginx's welcome page, reached via the shared localhost
docker rm -f web && docker network rm team-a team-b
Key takeaways
bridge(user-defined) is the right default for single-host applications.hostandnoneare the two extremes: no isolation versus no connectivity.overlayspans hosts;macvlanandipvlanput containers directly on the physical network.--network container:<name>shares an existing container's network stack, includinglocalhost.
🧪 Practice
- (Beginner) List the networks on your machine with
docker network lsand identify the driver and scope of each one. Try to create a second network with thehostdriver and read the error.- (Intermediate) Run a Redis container, then start a second container with
--network container:<redis>and connect to Redis onlocalhost:6379. Explain why no port publishing or network was needed.- Interview question: "A team wants their containers to receive IP addresses from the office DHCP range and be reachable from every desktop. Which driver would you choose and what could go wrong?" Hint: think about MAC addresses, switch port security, Wi-Fi interfaces, and the fact that Docker, not the DHCP server, assigns the addresses.
docker0 Bridge
Why a bridge. Every container has its own network namespace, which starts out completely disconnected, like a computer with no cable. To connect several containers to each other and to the outside, Docker needs two things: a virtual switch that all containers plug into, and virtual cables that cross the namespace boundary. Linux provides both: a bridge device is a software Ethernet switch, and a veth pair is a virtual cable with two ends, where anything sent into one end comes out of the other.
When the daemon starts, it creates the bridge docker0 for the default bridge network and gives it the address 172.17.0.1/16. For each container that joins the network, Docker:
- Creates a veth pair.
- Moves one end into the container's namespace and renames it
eth0. - Attaches the other end (named
vethXXXXXXX) todocker0. - Assigns the container an IP from the subnet and sets its default route via
172.17.0.1.
HOST (root network namespace)
+----------------------------------------------------------------------+
| |
| eth0 (192.168.1.50) <--- NAT (MASQUERADE) ---+ |
| | ip_forward = 1 |
| docker0 (bridge, 172.17.0.1/16) |
| +-------------+-----------------+ |
| | | |
| veth3f1a2b9 veth7c9d0e1 |
+----------|-------------------------------|---------------------------+
| (veth pair = virtual cable) |
+----------|-----------+ +-----------|----------+
| eth0 172.17.0.2 | | eth0 172.17.0.3 |
| default via .0.1 | | default via .0.1 |
| container "web" | | container "api" |
+----------------------+ +----------------------+
Traffic between containers stays inside the bridge, like frames on a physical switch. Traffic to the internet is routed by the host (IP forwarding must be enabled) and masqueraded (source NAT) behind the host's IP, because nothing outside knows the private 172.17.0.0/16 subnet. User-defined bridge networks work exactly the same way, but their bridges are named br-<network-id>.
Example: following the virtual cable
# The bridge exists even with no containers (state DOWN until something is attached)
ip addr show docker0
# 4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN ...
# inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
docker run -d --name web nginx:1.27-alpine
# Host side: a veth interface is now a port of docker0
ip link show master docker0
# 12: veth3f1a2b9@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> ... master docker0 state UP
# ^^^^ "@if11": the peer is interface index 11 in another namespace
# Container side: eth0 is index 11, and its peer is index 12 on the host
docker exec web ip addr show eth0
# 11: eth0@if12: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...
# inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
docker exec web ip route
# default via 172.17.0.1 dev eth0
# 172.17.0.0/16 dev eth0 scope link src 172.17.0.2
# Requirements for outbound traffic
cat /proc/sys/net/ipv4/ip_forward # 1 (Docker enables it)
sudo iptables -t nat -S POSTROUTING | grep 172.17
# -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
docker rm -f web
Changing the default subnet. 172.17.0.0/16 sometimes collides with a corporate VPN or office network, making those destinations unreachable from the host. Change it in /etc/docker/daemon.json and restart the daemon:
{
"bip": "10.200.0.1/24"
}
bip ("bridge IP") sets the address of docker0 and implicitly the subnet of the default network. Pools for user-defined networks are configured separately (see User-Defined Bridge Networks).
Docker Desktop note. On macOS and Windows, containers run inside a Linux VM, so docker0 and the veth interfaces exist in that VM, not on your machine. ip link on the host will not show them.
Key takeaways
docker0is a Linux bridge (a virtual switch) backing the defaultbridgenetwork, with gateway172.17.0.1.- Each container connects through a veth pair:
eth0inside,vethXXXXon the bridge. - Outbound traffic relies on IP forwarding plus a
MASQUERADErule; inbound traffic needs port publishing. - User-defined bridges use the same mechanism with
br-<id>devices; change the default subnet withbip.
🧪 Practice
- (Beginner) Start two containers and identify which
vethinterface on the host belongs to each, using the@ifNindexes (orcat /sys/class/net/eth0/iflinkinside the container).- (Intermediate) Run
sudo tcpdump -ni docker0 icmpon the host, then ping one container from another and from a container to1.1.1.1. Explain which packets you see and why the outbound ones leave the host with a different source address.- Interview question: "After connecting to the company VPN, developers can no longer reach some internal servers, but only on machines running Docker. What is likely happening?" Hint: compare the VPN's routes with the subnets of
docker0and thebr-*bridges (ip route), and look atbipanddefault-address-pools.
Port Mapping Internals
Why it is needed. A container's IP (172.17.0.2) is private to the host. A client on another machine cannot route to it, and on Docker Desktop not even the host can. Publishing with -p 8080:80 (see Port Publishing) tells Docker to make host port 8080 forward to container port 80. This topic looks at how that forwarding happens, because the mechanism explains many surprises: firewall bypass, the extra docker-proxy processes, and "port is already allocated" errors.
Three cooperating pieces
| Piece | Where | Role |
|---|---|---|
| DNAT rule | nat table, DOCKER chain | Rewrites the destination of incoming packets from host:8080 to 172.17.0.2:80 |
| Filter rule | filter table, DOCKER chain (via FORWARD) | Allows the rewritten packets to be forwarded to the bridge |
docker-proxy | A userland process per published port and protocol | Holds the host port (so nothing else can take it) and relays traffic that NAT rules cannot handle, such as connections to 127.0.0.1 |
Path of a packet from a remote client
client 198.51.100.7:51000 --> host eth0 192.168.1.50:8080
|
v nat PREROUTING -> DOCKER chain
| DNAT: dst 192.168.1.50:8080 ==> 172.17.0.2:80
v routing decision: 172.17.0.2 is behind docker0, so the packet is FORWARDED (not INPUT)
|
v filter FORWARD -> DOCKER-USER -> Docker's chains -> ACCEPT tcp dpt:80 to 172.17.0.2
|
v docker0 --> veth --> container eth0, nginx sees src 198.51.100.7, dst 172.17.0.2:80
|
reply: conntrack remembers the translation and rewrites the source back to 192.168.1.50:8080
Path of a connection from the host to localhost:8080. Packets to 127.0.0.1 never pass through PREROUTING, and the kernel by default refuses to route loopback addresses to another interface. Here docker-proxy does the work: it listens on the host port, accepts the connection, and opens a second connection to 172.17.0.2:80.
curl localhost:8080 --> docker-proxy (listening on 0.0.0.0:8080)
\--> new TCP connection --> 172.17.0.2:80
(nginx sees the connection coming from the gateway, 172.17.0.1)
Example: inspecting the mechanism
docker run -d --name web -p 8080:80 nginx:1.27-alpine
# 1. DNAT rule: traffic for local addresses enters the DOCKER chain...
sudo iptables -t nat -S PREROUTING
# -A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER
# ...and is rewritten to the container (except traffic that comes from docker0 itself)
sudo iptables -t nat -S DOCKER
# -A DOCKER ! -i docker0 -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.17.0.2:80
# 2. Filter rule: allow forwarding to the container port (note: 80, not 8080, after DNAT)
sudo iptables -S DOCKER | grep 172.17.0.2
# -A DOCKER -d 172.17.0.2/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT
# 3. The userland proxy owns the host port
sudo ss -lntp | grep 8080
# LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("docker-proxy",pid=5123,fd=7))
ps -o args= -C docker-proxy
# /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 8080 -container-ip 172.17.0.2 -container-port 80 ...
# Active translations tracked by conntrack (package "conntrack")
curl -s localhost:8080 >/dev/null
sudo conntrack -L -p tcp --dport 80 2>/dev/null | head -n 3
docker rm -f web
Chain layouts differ slightly between Engine versions, and the rules are written to nftables instead of iptables when that firewall backend is configured (see iptables and Docker), but the three roles stay the same.
Disabling the userland proxy. With "userland-proxy": false in daemon.json, Docker relies on NAT alone. It adds hairpin rules and enables the route_localnet kernel setting so that localhost traffic can be DNATed. This saves one process per port and preserves client source addresses in more cases, but some edge cases (for example IPv6 loopback when the container has no IPv6) behave differently. The default remains true.
Docker Desktop. Published ports are forwarded twice: Docker Desktop's networking component listens on your macOS or Windows host and passes the connection into the VM, where the mechanism above applies.
Key takeaways
- Publishing combines a DNAT rule, a forward-accept rule, and usually a
docker-proxyprocess. - Published traffic is DNATed in
PREROUTINGand then forwarded, so it never traverses the host'sINPUTchain, which is where most host firewalls filter. docker-proxyreserves the host port and handles loopback connections that NAT cannot.- After DNAT, firewall rules see the container port, not the published host port.
🧪 Practice
- (Beginner) Publish nginx on two host ports (
-p 8080:80 -p 9090:80) and list the corresponding DNAT rules anddocker-proxyprocesses.- (Intermediate) Compare the client address nginx logs (
docker logs) for a request tolocalhost:8080from the host and for a request to<host-LAN-IP>:8080from another machine. Explain the difference using the two packet paths above.- Interview question: "Why does
ss -lntpon a Docker host show a process listening on a published port, even though the traffic is forwarded by iptables?" Hint: consider what would happen if another program tried to bind that port, and how connections to127.0.0.1reach the container.
iptables and Docker
Why Docker touches the firewall. NAT for outbound traffic, DNAT for published ports, and isolation between networks are all implemented in the kernel's packet filter (netfilter). Docker therefore manages firewall rules on your behalf, creating its own chains and rewriting them whenever networks and containers change. This is convenient, but it is also the source of Docker's most famous surprise: published ports bypass host firewalls such as ufw.
The chains Docker manages (iptables backend)
| Table | Chain | Purpose |
|---|---|---|
nat | DOCKER | DNAT rules for published ports (reached from PREROUTING and OUTPUT) |
nat | POSTROUTING | MASQUERADE rules so containers can reach the outside |
filter | DOCKER-USER | Yours. Evaluated first for forwarded traffic; Docker creates it but never adds rules to it |
filter | DOCKER | Accept rules for published container ports |
filter | DOCKER-ISOLATION-STAGE-1/2 and, in newer Engines, chains such as DOCKER-FORWARD, DOCKER-BRIDGE, DOCKER-CT (names vary by version) | Isolation between bridge networks, connection tracking, internal networks |
Why ufw does not see published traffic
incoming packet for host:5432
|
v
nat PREROUTING --(Docker DNAT)--> dst becomes 172.17.0.5:5432
|
v
routing: is the destination a local address?
| |
no (container IP) yes (host service)
v v
filter FORWARD filter INPUT <-- ufw / firewalld rules live here
-> DOCKER-USER <-- your rules (never reached by published traffic)
-> Docker chains -> ACCEPT
|
v
container
Restricting published ports correctly. Put rules in DOCKER-USER. Remember two details: replies to connections that containers initiated also pass through FORWARD, so let established traffic through first; and after DNAT the destination port is the container port, so match the original port with conntrack.
# Let replies and related traffic through before any DROP rule
sudo iptables -I DOCKER-USER 1 -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
# Only the office network may reach containers from the external interface eth0
sudo iptables -I DOCKER-USER 2 -i eth0 ! -s 203.0.113.0/24 -j DROP
# Block a specific published HOST port (5432) from outside, whatever container port it maps to
sudo iptables -I DOCKER-USER 2 -i eth0 -p tcp \
-m conntrack --ctorigdstport 5432 --ctdir ORIGINAL -j DROP
sudo iptables -L DOCKER-USER -n -v --line-numbers
# Rules added by hand are lost on reboot: persist them (iptables-persistent, a systemd unit, ...)
Daemon options that control firewall behavior
daemon.json key | Default | Effect |
|---|---|---|
"iptables" | true | false stops Docker from managing IPv4 rules; you must then provide NAT and forwarding yourself, otherwise containers lose connectivity |
"ip6tables" | true (Engine 27+) | Same for IPv6 |
"ip-forward" | true | Docker enables net.ipv4.ip_forward at startup |
"userland-proxy" | true | Use docker-proxy for published ports |
"firewall-backend" | "iptables" | "nftables" writes native nftables rules instead (experimental, Engine 29+) |
Example: watching rules appear and disappear
sudo iptables -t nat -S DOCKER > /tmp/before.txt
docker run -d --name pg -p 127.0.0.1:5432:5432 -e POSTGRES_PASSWORD=secret postgres:16
sudo iptables -t nat -S DOCKER | diff /tmp/before.txt -
# > -A DOCKER -d 127.0.0.1/32 ! -i docker0 -p tcp -m tcp --dport 5432 -j DNAT --to-destination 172.17.0.2:5432
# ^^^^^^^^^^^^ binding to 127.0.0.1 restricts the DNAT to that destination address
docker rm -f pg
sudo iptables -t nat -S DOCKER | diff /tmp/before.txt - && echo "rules removed"
Pitfalls
- Flushing the firewall.
iptables -F,firewall-cmd --reload(with older integrations), or a configuration tool that rewrites the whole ruleset deletes Docker's chains. Containers silently lose connectivity untildockerdis restarted. - firewalld. Docker registers its bridges in a
dockerzone; rules added to other zones do not filter container traffic. - IPv6. If IPv6 is enabled, apply equivalent
ip6tablesrules toDOCKER-USER.
Key takeaways
- Docker implements NAT, publishing, and network isolation with netfilter rules in its own chains.
- Published traffic is forwarded, not delivered locally, so
INPUT-based firewalls (ufw, manyfirewalldsetups) do not filter it. - Put custom restrictions in
DOCKER-USER, allow established traffic first, and use conntrack to match the original host port. - Never disable
iptablesmanagement or flush the ruleset without understanding the consequences; restartdockerdto restore its rules.
🧪 Practice
- (Beginner) Run
sudo iptables -t nat -Sandsudo iptables -Sbefore and after starting a container with-p 8080:80, and annotate every rule that changed.- (Intermediate) On a VM with
ufwenabled and port 8080 denied, publish nginx on 8080 and show it is still reachable from another machine. Then block it with aDOCKER-USERrule matching the original destination port, and verify that the container can still reach the internet.- Interview question: "A colleague added
iptables -I DOCKER-USER -i eth0 -j DROPto protect the containers, and nowapt-get updateinside containers hangs. Why?" Hint: follow the path of the reply packets from the package mirror back to the container.
<a id="72-network-drivers"></a>
7.2 Network Drivers
This section examines each built-in network driver in detail: how it works, when to choose it, and what its limitations are.
Default Bridge Network
Why it still exists. The network named bridge (backed by docker0) predates user-defined networks. Every container started without --network joins it, so existing scripts and tutorials keep working. It is fine for a quick single container, but it has deliberate limitations that make it a poor choice for applications made of several containers.
Characteristics
| Property | Default bridge network |
|---|---|
| Subnet | 172.17.0.0/16 (configurable only daemon-wide via bip) |
| Name resolution | None: containers can reach each other only by IP |
/etc/resolv.conf | Copy of the host's resolvers (loopback resolvers removed; falls back to public DNS if nothing is left) |
| Container-to-container traffic | Allowed between all containers on it (daemon option "icc": false disables it) |
| Configuration changes | Require a daemon restart |
| Legacy linking | --link (deprecated) injects /etc/hosts entries and environment variables |
Analogy. The default bridge is a hotel lobby: everyone who arrives without a reservation ends up there, everyone can talk to everyone, and nobody has a name tag. A user-defined network is a private meeting room with a guest list and name tags.
Example: the name resolution limitation
docker run -d --name a alpine sleep 1d
docker run -d --name b alpine sleep 1d
# Both are on "bridge" without asking for it
docker inspect -f '{{range $net, $_ := .NetworkSettings.Networks}}{{$net}}{{end}}' a
# bridge
ADDR_A=$(docker inspect -f '{{.NetworkSettings.Networks.bridge.IPAddress}}' a)
docker exec b ping -c 1 "$ADDR_A" # works: 64 bytes from 172.17.0.2 ...
docker exec b ping -c 1 a # ping: bad address 'a'
# The legacy workaround: --link writes a static /etc/hosts entry (do not use in new setups)
docker run --rm --link a:db alpine cat /etc/hosts | tail -n 2
# 172.17.0.2 db 4b1f6c2e9a3d a
# 172.17.0.4 e8a1c2d3f4b5
docker rm -f a b
The --link entry is static: if container a restarts and receives a different IP, the linked container keeps the stale address.
Key takeaways
- Containers without
--networkjoin the defaultbridgenetwork ondocker0. - It provides no DNS-based name resolution and puts all unrelated containers on one shared segment.
--linkis a deprecated workaround; use a user-defined network instead.- Reserve the default bridge for quick, single-container experiments.
🧪 Practice
- (Beginner) Start two containers on the default bridge and show that they can ping each other by IP but not by name. Then repeat on a user-defined network.
- (Intermediate) Compare
/etc/resolv.confinside a container on the default bridge with one on a user-defined network, and explain each difference.- Interview question: "Why didn't Docker simply add DNS to the default bridge network instead of introducing user-defined networks?" Hint: think about backward compatibility with
--link, and the security implications of every container on a host sharing one namespace of names.
User-Defined Bridge Networks
Why they are the recommended default. A user-defined bridge network uses the same bridge, veth, and NAT machinery as docker0, but adds the features multi-container applications need: containers find each other by name, unrelated applications are isolated from each other, and containers can be attached and detached at runtime. Docker Compose creates one for every project automatically.
Default vs user-defined bridge
| Feature | Default bridge | User-defined bridge |
|---|---|---|
| DNS resolution by container name and alias | No | Yes (embedded DNS at 127.0.0.11) |
| Isolation | All default-bridge containers share one segment | Only containers on that network can talk to each other |
| Per-network settings (subnet, MTU, internal, ICC) | No (daemon-wide only) | Yes |
Static IP with --ip | No | Yes, if the network has a user-specified subnet |
| Network-scoped aliases | No | Yes |
Creating and configuring
# Simplest form: Docker picks a free subnet from its address pools
docker network create appnet
# Fully specified network; each option explained:
# --subnet / --gateway : the address plan
# --ip-range : dynamic allocation only from 10.10.0.128-255, leaving the lower half for static IPs
# bridge.name : a readable host interface name instead of br-<id>
# driver.mtu : lower MTU, e.g. when the host sits behind a VPN or tunnel
docker network create \
--driver bridge \
--subnet 10.10.0.0/24 \
--gateway 10.10.0.1 \
--ip-range 10.10.0.128/25 \
-o com.docker.network.bridge.name=br-shop \
-o com.docker.network.driver.mtu=1400 \
--label project=shop \
shopnet
# Static IP from the reserved lower half
docker run -d --name db --network shopnet --ip 10.10.0.10 -e POSTGRES_PASSWORD=secret postgres:16
docker run --rm --network shopnet alpine sh -c 'getent hosts db; ip -4 addr show eth0 | grep inet'
# 10.10.0.10 db
# inet 10.10.0.128/24 brd 10.10.0.255 scope global eth0 <- dynamic address from --ip-range
# Inspect and manage
docker network inspect shopnet -f '{{json .IPAM.Config}}'
docker network ls --filter label=project=shop
docker rm -f db
docker network rm shopnet
docker network prune -f # remove all networks no container uses
Address pools. Without --subnet, Docker takes subnets from its default address pools: 172.17.0.0/16 through 172.31.0.0/16 and then 192.168.0.0/16 split into /20 blocks, about 30 networks in total. When they are exhausted, docker network create fails with "could not find an available, non-overlapping IPv4 address pool". Hosts running many Compose projects, or networks colliding with 192.168.x.x home and office LANs, are common reasons to define your own pools:
{
"default-address-pools": [
{ "base": "10.201.0.0/16", "size": 24 }
]
}
This gives 256 networks of 254 usable addresses each, all inside 10.201.0.0/16.
Key takeaways
- Always use a user-defined bridge for multi-container applications on one host.
- It adds name-based DNS, per-application isolation, and per-network configuration on top of the default bridge's mechanics.
--subnet,--ip-range,--gateway, and-odriver options control addressing and interface details;--iprequires a user-specified subnet.- Configure
default-address-poolswhen networks run out or collide with existing LAN or VPN ranges.
🧪 Practice
- (Beginner) Create a network
blog, runmysql:8.4andwordpresson it, and configure WordPress to reach the database by container name.- (Intermediate) Configure
default-address-poolswith a/16base and/26size, restart Docker, create five networks, and verify the allocated subnets withdocker network inspect. How many networks can the pool hold?- Interview question: "Your CI server suddenly fails with 'could not find an available, non-overlapping IPv4 address pool'. What happened and how do you fix it, both immediately and permanently?" Hint: consider leftover networks from aborted Compose runs,
docker network prune, and the size of the default pools.
Host Network
Why skip isolation. Every bridge network costs something: a veth hop, NAT, conntrack entries, and the need to publish each port explicitly. For most applications this is negligible. For some, it is not: a load balancer handling hundreds of thousands of packets per second, an application opening many dynamic ports (SIP/RTP for VoIP, passive FTP), or a monitoring agent that must see the host's real interfaces. With --network host, the container does not get its own network namespace. It uses the host's interfaces, IP addresses, routing table, and ports directly.
Analogy. Bridge networking gives each container its own apartment with a mailbox at the building entrance (port mapping). Host networking lets the container live in your own flat: no mailbox needed, but it can see and use everything you have.
Trade-offs
| Advantage | Disadvantage |
|---|---|
| No NAT or veth overhead | No network isolation: the container sees all host interfaces |
| No port publishing; any port the app opens is reachable | Port conflicts with host services and other host-mode containers |
| Large or dynamic port ranges are easy | Two replicas of the same app cannot use the same port on one host |
Host services are reachable on localhost | -p and -P are ignored |
| Client source IPs are preserved | Behaves differently on Docker Desktop (see below) |
Other namespaces still apply: the container keeps its own filesystem, process tree, and cgroup limits.
Example
# nginx binds directly to port 80 on the host
docker run -d --name hostweb --network host nginx:1.27-alpine
sudo ss -lntp | grep ':80 '
# LISTEN 0 511 0.0.0.0:80 0.0.0.0:* users:(("nginx",pid=6012,...)) <- no docker-proxy involved
curl -sI http://localhost | head -n 1 # HTTP/1.1 200 OK
# The container sees the host's interfaces and hostname
docker run --rm --network host alpine ip -4 addr | grep inet
docker run --rm --network host alpine hostname # same as the host's hostname
# Publishing is meaningless in host mode
docker run --rm --network host -p 8080:80 alpine true
# WARNING: Published ports are discarded when using host network mode
# A second host-mode nginx fails: port 80 is already taken
docker run --rm --network host nginx:1.27-alpine
# nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address in use)
docker rm -f hostweb
Docker Desktop. Containers run in a Linux VM, so by default "host" means the VM's network, not your Mac's or PC's. Docker Desktop 4.34 and later offers an opt-in host networking setting that forwards ports bound by host-mode containers to your machine; it works at layer 4 (TCP/UDP) only and does not expose your machine's interfaces to the container.
Key takeaways
--network hostremoves network isolation: the container shares the host's interfaces, IPs, and port space.- It avoids NAT and veth overhead and suits high-throughput or many-port workloads.
- Port publishing is ignored, and port conflicts become your responsibility.
- On Docker Desktop, "host" refers to the VM unless the host networking feature is enabled.
🧪 Practice
- (Beginner) Run
python:3.13-slim python -m http.server 8765with--network host, reach it from the host without-p, and confirm withss -lntpwhich process owns the port.- (Intermediate) Benchmark the same nginx container on a user-defined bridge with
-pand in host mode using a load tool such aswrkorhey. Report requests per second and latency, and explain the difference (or the lack of one).- Interview question: "When would you deliberately choose host networking in production, and what compensating controls would you add?" Hint: consider performance-critical proxies and node agents, then think about firewalling host ports, running as non-root, and dropping
NET_ADMIN/NET_RAW.
None Network
Why have no network. The safest network connection is no connection. A container that converts documents, processes untrusted uploads, runs tests that must not reach the internet, or generates build artifacts often needs no network at all. With --network none, the container gets its own network namespace containing only the loopback interface. Nothing goes in or out, so even a compromised process cannot exfiltrate data or download a payload over the network.
Characteristics
- Only
lo(127.0.0.1) exists; there is noeth0, no routes, and no DNS server. - Processes inside can still talk to each other over
localhost. - Volumes and bind mounts still work, so data can be exchanged through files.
- It is also a starting point for custom networking: external tooling can move interfaces into the namespace (this is how some orchestrators wired up containers before CNI support).
Example
docker run --rm --network none alpine ip addr
# 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 ...
# inet 127.0.0.1/8 scope host lo
# (no eth0)
docker run --rm --network none alpine wget -T 3 -qO- http://example.com
# wget: bad address 'example.com'
# Typical use: process untrusted input with no network access
mkdir -p in out && echo '# Title' > in/doc.md
docker run --rm --network none \
-v "$PWD/in:/in:ro" \
-v "$PWD/out:/out" \
pandoc/core:3.5 /in/doc.md -o /out/doc.html # converts the file; cannot phone home
ls out/
Key takeaways
--network nonegives a container loopback only: no external connectivity at all.- It is a simple, strong control for batch jobs and processing untrusted data.
- Data exchange must happen through mounts, not the network.
- It can serve as an empty namespace for fully custom network setups.
🧪 Practice
- (Beginner) Start a container with
--network noneand verify that it has no routes (ip route) and that DNS lookups fail.- (Intermediate) Write a script that runs your project's unit tests inside a
--network nonecontainer with the source code mounted read-only. Which tests fail, and what does that reveal about hidden network dependencies?- Interview question: "How would you design a service that converts user-uploaded files with a third-party tool you do not fully trust?" Hint: combine
--network nonewith read-only mounts, resource limits, dropped capabilities, and a non-root user.
Overlay Network
Why it is needed. A bridge network ends at the host's edge. When containers on different machines must talk directly, as if they were on one switch, you need a network that spans hosts. The overlay driver builds a virtual layer 2 network on top of the existing host network (the underlay). Frames sent by a container are wrapped in UDP packets (VXLAN encapsulation), carried to the destination host, and unwrapped there.
Analogy. It is like sending a letter inside another envelope. The inner letter is addressed to 10.0.9.5 (a container IP that the physical network has never heard of). The outer envelope is addressed to 192.168.56.12 (the host that runs that container). The postal service only reads the outer envelope.
Host 1 (192.168.56.11) Host 2 (192.168.56.12)
+---------------------------+ +---------------------------+
| container A 10.0.9.4 | | container B 10.0.9.5 |
| | | | ^ |
| overlay br0 + vxlan ----+-- UDP 4789 -------+-> vxlan + br0 |
| [outer: 192.168.56.11 -> 192.168.56.12] [inner: 10.0.9.4 -> 10.0.9.5]
+---------------------------+ +---------------------------+
\______________ underlay network (LAN, VPC) ______________/
Requirements. Overlay networks need Swarm mode, which provides the control plane: membership, distribution of network state and encryption keys, and service discovery. These ports must be open between nodes:
| Port | Protocol | Purpose |
|---|---|---|
| 2377 | TCP | Cluster management (to managers) |
| 7946 | TCP and UDP | Node communication and gossip about network state |
| 4789 | UDP | VXLAN data traffic |
| IP protocol 50 (ESP) | Only for encrypted overlays |
Example: two hosts, one network
# --- node1 (becomes the manager) ---
docker swarm init --advertise-addr 192.168.56.11
# prints a "docker swarm join --token SWMTKN-1-... 192.168.56.11:2377" command
# --- node2 ---
docker swarm join --token SWMTKN-1-... 192.168.56.11:2377
# --- node1 ---
# --attachable lets plain "docker run" containers join (by default only Swarm services can)
docker network create -d overlay --attachable --subnet 10.0.9.0/24 multihost
docker run -d --name svc1 --network multihost nginx:1.27-alpine
# --- node2 --- (the network is created here on demand when a container needs it)
docker run --rm --network multihost alpine wget -qO- http://svc1 | head -n 4
# nginx welcome page served from node1, resolved by name across hosts
# --- Swarm services get a virtual IP (VIP) load-balanced across their tasks ---
docker service create --name api --replicas 3 --network multihost traefik/whoami
docker run --rm --network multihost alpine nslookup api # one VIP
docker run --rm --network multihost alpine nslookup tasks.api # one IP per task (3)
Networks Swarm creates automatically
| Network | Driver | Purpose |
|---|---|---|
ingress | overlay | Carries traffic for ports published by services (see Routing Mesh) |
docker_gwbridge | bridge | Connects containers on overlay networks to the outside world on each node |
Encryption and MTU
- Overlay traffic is unencrypted by default.
docker network create -d overlay --opt encrypted secure-netencrypts data traffic between nodes with IPsec. It costs CPU and requires ESP to be allowed by firewalls and cloud security groups. - VXLAN adds 50 bytes of headers, so the overlay MTU defaults to 1450 on a 1500-byte underlay. If the underlay itself is smaller (some clouds, VPNs), set
--opt com.docker.network.driver.mtu=<value>accordingly, otherwise large packets are silently dropped.
Key takeaways
- Overlay networks connect containers across hosts by encapsulating layer 2 frames in VXLAN (UDP 4789).
- They require Swarm mode for the control plane;
--attachableallows standalone containers to join. - Swarm services get a VIP with built-in load balancing;
tasks.<service>returns individual task IPs. - Open the required ports between nodes, consider
--opt encryptedon untrusted networks, and mind the MTU.
🧪 Practice
- (Beginner) Using two VMs (or Play with Docker), create a Swarm and an attachable overlay network, then ping a container on node1 from a container on node2 by name.
- (Intermediate) Capture traffic on the underlay with
sudo tcpdump -ni eth0 udp port 4789while the containers communicate. Identify the outer and inner IP headers. Repeat with an encrypted overlay and describe what changes.- Interview question: "Containers on an overlay network can ping each other, but HTTP requests with large responses hang. What is your first suspect?" Hint: compare the underlay MTU with the VXLAN overhead and check for blocked ICMP "fragmentation needed" messages.
Macvlan and IPvlan
Why put containers on the physical LAN. Some workloads expect to be first-class citizens of the physical network: legacy applications licensed to a MAC address, network appliances, monitoring tools that sniff traffic, or services that other devices must reach on a dedicated IP without port mapping. Macvlan and IPvlan attach containers directly to a host interface (the parent), bypassing the bridge and NAT. To the rest of the network, each container looks like a separate machine.
How they differ
| Macvlan | IPvlan L2 | IPvlan L3 | |
|---|---|---|---|
| MAC address per container | Own, unique MAC | Shares the parent's MAC | Shares the parent's MAC |
| Visible on the LAN as | Separate devices | Separate IPs behind one MAC | A routed subnet behind the host |
| Broadcast/multicast (ARP, mDNS) | Yes | Yes | No |
| Needs promiscuous mode / extra MACs allowed | Often | No | No |
| Router configuration | None | None | Static route for the container subnet via the host |
| Typical blocker | Switch port security, Wi-Fi, cloud anti-spoofing |
Macvlan (bridge mode) IPvlan L3
LAN switch router: 10.50.0.0/24 via 192.168.1.50
| |
eth0 (host) MAC aa:..:01 eth0 (host) 192.168.1.50 MAC aa:..:01
|-- container 1 192.168.1.200 MAC 02:..:c8 |-- container 1 10.50.0.10 (same MAC)
|-- container 2 192.168.1.201 MAC 02:..:c9 |-- container 2 10.50.0.11 (same MAC)
Example: a macvlan network on the office LAN
# LAN: 192.168.1.0/24, router 192.168.1.1. Reserve 192.168.1.192/27 for containers
# and exclude it from the DHCP server's range: Docker assigns these IPs itself, not DHCP.
# --aux-address : keep 192.168.1.223 free for the host shim interface (below)
# parent=eth0 : the physical NIC; use eth0.10 to place containers in 802.1Q VLAN 10
docker network create -d macvlan \
--subnet 192.168.1.0/24 \
--gateway 192.168.1.1 \
--ip-range 192.168.1.192/27 \
--aux-address 'host-shim=192.168.1.223' \
-o parent=eth0 \
lan
docker run -d --name kiosk --network lan --ip 192.168.1.200 nginx:1.27-alpine
# From another machine on the LAN: curl http://192.168.1.200 -> works, no -p needed
# From the Docker host itself: curl http://192.168.1.200 -> times out (see below)
The host cannot reach its own macvlan containers. By design, the kernel does not pass traffic between a parent interface and its macvlan children. The common workaround is a macvlan "shim" interface on the host:
sudo ip link add lan-shim link eth0 type macvlan mode bridge
sudo ip addr add 192.168.1.223/32 dev lan-shim
sudo ip link set lan-shim up
sudo ip route add 192.168.1.192/27 dev lan-shim # reach containers through the shim
curl -sI http://192.168.1.200 | head -n 1 # HTTP/1.1 200 OK
Example: an IPvlan L3 network
# Containers live in 10.50.0.0/24, routed by the host; no ARP on the LAN for these IPs
docker network create -d ipvlan \
--subnet 10.50.0.0/24 \
-o parent=eth0 \
-o ipvlan_mode=l3 \
routed-net
docker run -d --name svc --network routed-net --ip 10.50.0.10 nginx:1.27-alpine
# On the LAN router: add a static route 10.50.0.0/24 via 192.168.1.50 (the Docker host)
Pitfalls
- Wi-Fi. Most wireless drivers and access points drop frames from MAC addresses that did not associate, so macvlan rarely works over Wi-Fi. Use IPvlan instead.
- Cloud VPCs. AWS, Azure, and GCP drop traffic from unknown MAC and IP addresses. Use the provider's own mechanisms (secondary IPs, CNI plugins) instead.
- Switch limits. Port security may cap the number of MACs per port and shut the port down.
- No published ports and no Docker NAT. Firewalling must be done on the network, not with
-pbindings.
Key takeaways
- Macvlan gives each container its own MAC and IP on the physical network; IPvlan shares the parent's MAC.
- Use IPvlan L2 where extra MACs are forbidden, and IPvlan L3 for a routed design without broadcast traffic.
- Docker assigns the IPs: reserve a range outside the LAN's DHCP pool.
- The host cannot talk to its macvlan containers without a shim interface; Wi-Fi and cloud networks usually block macvlan.
🧪 Practice
- (Beginner) On a Linux VM with a bridged network adapter, create a macvlan network and run nginx on a LAN IP. Reach it from another device on the LAN, then try (and fail) from the host.
- (Intermediate) Add the shim interface to restore host-to-container connectivity. Then recreate the network as IPvlan L2 and compare the MAC addresses seen in the router's or another machine's ARP table (
ip neigh).- Interview question: "A security appliance vendor ships its product as a container that must capture traffic on a mirrored switch port and have its own management IP. How would you network it?" Hint: consider separate parent interfaces for capture and management, macvlan versus host mode, and the capabilities packet capture requires.
<a id="73-container-communication"></a>
7.3 Container Communication
This section focuses on how containers find and reach each other: service discovery, naming, multi-network topologies, isolation, and diagnosing what goes wrong.
Embedded DNS and Service Discovery
Why names instead of IPs. Container IP addresses are assigned dynamically and change whenever a container is recreated. Hard-coding 172.18.0.3 into a configuration breaks on the next deployment. Service discovery means finding a service by a stable name instead. On user-defined networks, Docker provides it with an embedded DNS server that every container reaches at 127.0.0.11.
How it works
- Docker writes
nameserver 127.0.0.11into the container's/etc/resolv.conf. - Inside the container's namespace, Docker adds NAT rules that redirect
127.0.0.11:53to a socket thatdockerdlistens on in that namespace. dockerdanswers queries for container names, network aliases, and short container IDs of containers that share a network with the requester.- Any other name is forwarded to the upstream resolvers: the host's resolvers (Docker reads the real list from
systemd-resolvedwhen the host uses the127.0.0.53stub), or the servers given with--dns.
container "api" (network appnet)
getaddrinfo("db") --> /etc/resolv.conf: nameserver 127.0.0.11
|
v (NAT inside the container namespace)
dockerd embedded DNS
| \
"db" is on appnet? "example.com"?
yes -> 172.18.0.3 forward to upstream (host resolvers / --dns)
Names are network-scoped: a container resolves only the names of containers it shares at least one network with.
Example
docker network create appnet
docker run -d --name db --network appnet -e POSTGRES_PASSWORD=secret postgres:16
docker run --rm --network appnet alpine cat /etc/resolv.conf
# nameserver 127.0.0.11
# options ndots:0
docker run --rm --network appnet alpine nslookup db
# Server: 127.0.0.11
# Address: 127.0.0.11:53
# Name: db
# Address: 172.18.0.2
# External names are forwarded upstream
docker run --rm --network appnet alpine nslookup docker.com | tail -n 2
# The redirection rules inside the container's namespace
PID=$(docker inspect -f '{{.State.Pid}}' db)
sudo nsenter -t "$PID" -n iptables -t nat -S DOCKER_OUTPUT
# -A DOCKER_OUTPUT -d 127.0.0.11/32 -p tcp -m tcp --dport 53 -j DNAT --to-destination 127.0.0.11:39811
# -A DOCKER_OUTPUT -d 127.0.0.11/32 -p udp -m udp --dport 53 -j DNAT --to-destination 127.0.0.11:45123
docker rm -f db && docker network rm appnet
DNS-related docker run options
| Option | Effect |
|---|---|
--dns 10.0.0.53 | Upstream server for external names (on user-defined networks, 127.0.0.11 stays the container's resolver) |
--dns-search corp.example | Search domain appended to short names |
--dns-option ndots:2 | Resolver options written to resolv.conf |
--add-host api.local:10.0.0.7 | Static entry in the container's /etc/hosts |
--add-host host.docker.internal:host-gateway | Name for the Docker host (built in on Docker Desktop; needs this flag on Linux) |
--hostname | The container's own hostname, which by itself is not resolvable by other containers |
Pitfall: clients that cache DNS forever. Some software resolves a name once at startup. If the target container is recreated with a new IP, connections fail until the client restarts. nginx is the classic example: proxy_pass http://api:8000; resolves api only when the configuration loads. Force runtime resolution by using a variable together with a resolver directive:
server {
listen 80;
resolver 127.0.0.11 valid=10s; # Docker's embedded DNS, re-resolve every 10 s
location / {
set $upstream http://api:8000; # a variable forces resolution at request time
proxy_pass $upstream;
}
}
The JVM (networkaddress.cache.ttl) and connection pools have similar settings.
Key takeaways
- On user-defined networks, every container uses Docker's embedded DNS server at
127.0.0.11. - It resolves container names, aliases, and short IDs of containers on shared networks, and forwards everything else upstream.
- Name resolution is network-scoped and unavailable on the default bridge.
- Use
--dns,--dns-search, and--add-hostfor custom resolution, and make sure clients re-resolve names when containers are recreated.
🧪 Practice
- (Beginner) On a user-defined network, resolve another container by its name, by its short ID, and by its
--hostname. Which ones work?- (Intermediate) Put nginx in front of an API container using
proxy_pass http://api:8000;. Recreate the API container so it gets a new IP and observe the 502 errors. Then fix the configuration withresolverand a variable, and repeat the test.- Interview question: "How can a containerized application on Linux reach a database that runs directly on the Docker host, without hard-coding the host's IP?" Hint: look at the special
host-gatewayvalue for--add-host, and consider which host interface the database must listen on.
Network Aliases
Why extra names. A container name is unique per host and tied to one container. Often you want a name that describes a role rather than a specific container: search instead of elasticsearch-blue-2, or the same name for several interchangeable replicas. A network alias is an additional DNS name for a container, valid only on a specific network. Several containers can share an alias, in which case the embedded DNS returns all of their addresses (simple DNS round-robin).
Analogy. A container name is a person's legal name; an alias is a job title. "Ask the on-call engineer" keeps working when the person on call changes, and several people can share the title.
Setting aliases
# At start time (applies to the network given with --network)
docker run -d --name search-1 --network appnet --network-alias search traefik/whoami
# When connecting an existing container to a network (one alias per network)
docker network connect --alias cache appnet redis-primary
# Aliases do not work on the default bridge:
docker run --rm --network-alias foo alpine true
# docker: Error response from daemon: network-scoped alias is supported only for containers in user defined networks.
Example: round-robin across two replicas and a blue/green switch
docker network create searchnet
docker run -d --name search-1 --network searchnet --network-alias search traefik/whoami
docker run -d --name search-2 --network searchnet --network-alias search traefik/whoami
docker run --rm --network searchnet alpine nslookup search
# Name: search
# Address: 172.20.0.2
# Name: search
# Address: 172.20.0.3
# Requests are spread across replicas (whoami prints the serving container's hostname)
for i in 1 2 3 4; do
docker run --rm --network searchnet alpine wget -qO- http://search | grep Hostname
done
# Which DNS names does a container answer to on this network?
docker inspect -f '{{json .NetworkSettings.Networks.searchnet.DNSNames}}' search-1
# ["search-1","search","9c1e2f3a4b5d"]
# Blue/green: start "green" with the alias, then remove "blue" from the network
docker run -d --name search-green --network searchnet --network-alias search traefik/whoami
docker network disconnect searchnet search-1
docker network disconnect searchnet search-2
docker rm -f search-1 search-2 search-green && docker network rm searchnet
Aliases in Compose. Every Compose service name is automatically an alias on the project networks, which is why db:5432 works between services. Additional names go under networks.<name>.aliases:
services:
postgres:
image: postgres:16
networks:
backend:
aliases:
- db # reachable as "postgres" and "db" on backend
- db.internal
networks:
backend:
Limits of DNS round-robin. It is not a load balancer: there are no health checks, clients often cache or always pick the first address, and long-lived connections stick to one replica. Use a real proxy (nginx, HAProxy, Traefik) or Swarm's VIP mode when balancing matters.
Key takeaways
- A network alias is an extra DNS name for a container, scoped to one network.
- Several containers can share an alias; DNS then returns all their IPs (round-robin).
- Set aliases with
--network-aliasordocker network connect --alias; they require user-defined networks. - Compose service names are aliases; DNS round-robin is not health-aware load balancing.
🧪 Practice
- (Beginner) Give a Redis container the aliases
cacheandsessionson one network and verify that both names resolve to the same IP.- (Intermediate) Connect one container to two networks with a different alias on each. Show that clients on each network can resolve only the alias of their own network.
- Interview question: "Three API replicas share the alias
api, yet one of them receives almost all the traffic. Explain possible reasons." Hint: think about client-side DNS caching, connection pooling and keep-alive, and resolver libraries that sort or prefer addresses.
Connecting Containers to Multiple Networks
Why multiple networks. Real architectures have tiers. A reverse proxy must reach the application, the application must reach the database, but the proxy (exposed to the internet) should never reach the database directly. With Docker, you express this by creating one network per tier and connecting only the containers that bridge two tiers to both. Each network a container joins gives it another interface (eth0, eth1, ...) with its own IP.
internet
| -p 443:443
+------v------+
| proxy | network "frontend"
=====+=============+======+=============================
|
+------v------+
| api | (eth0 on frontend, eth1 on backend)
+------+------+
==========================+======+====================== network "backend" (--internal)
|
+------v------+
| db | unreachable from proxy
+-------------+
Commands
| Command | Purpose |
|---|---|
docker run --network frontend --network backend ... | Join several networks at creation (Engine 25+) |
docker network connect backend api | Attach a running container to another network |
docker network connect --ip 10.10.0.20 --alias svc backend api | Same, with a static IP and alias on that network |
docker network disconnect frontend api | Detach from a network without stopping the container |
docker run --network name=frontend,gw-priority=1 ... | Choose which network provides the default route (Engine 28+) |
Example
docker network create frontend
docker network create --internal backend
docker run -d --name db --network backend -e POSTGRES_PASSWORD=secret postgres:16
docker run -d --name api --network backend alpine sleep 1d
docker network connect frontend api # api now bridges both tiers
docker run -d --name proxy --network frontend -p 8080:80 nginx:1.27-alpine
# One IP per network
docker inspect -f '{{range $n, $c := .NetworkSettings.Networks}}{{$n}} {{$c.IPAddress}}{{"\n"}}{{end}}' api
# backend 172.21.0.3
# frontend 172.22.0.2
docker exec api nslookup db | tail -n 1 # resolves: shares "backend" with db
docker exec api nslookup proxy | tail -n 1 # resolves: shares "frontend" with proxy
docker exec proxy nslookup db # fails: proxy is not on backend
# Outbound traffic uses the default route of ONE network; here the non-internal one
docker exec api ip route
# default via 172.22.0.1 dev eth1 (interface names and order may vary)
docker rm -f db api proxy && docker network rm frontend backend
Pitfalls
- Interface order. Do not assume
eth0belongs to a particular network; look up interfaces by IP or subnet. - Default gateway. Only one network provides the default route. If it is an
--internalnetwork, the container has no internet access; usegw-priorityon newer Engines to choose explicitly. - Listening addresses. An application that binds to one specific IP is reachable only on that network; bind to
0.0.0.0to serve all of them. - Name collisions. If a name or alias exists on two networks with different containers, the answer depends on which network the query is resolved for; keep names unique.
Key takeaways
- Connect a container to several networks to build tiered topologies; it gets one interface and IP per network.
- Use
docker network connect/disconnectto change membership at runtime, or repeat--networkat creation. - Containers resolve only the names of containers on networks they share.
- Only one network supplies the default route; control it explicitly when it matters.
🧪 Practice
- (Beginner) Build the proxy/api/db topology above and prove with
pingornc -zvwhich containers can reach which.- (Intermediate) Without restarting anything, move
apifrom networkblueto networkgreenusingdocker network connectanddisconnect, while a client loop keeps resolving and calling it. Record how long resolution fails, if at all.- Interview question: "Why is connecting the reverse proxy directly to the database network a security problem, even though the proxy does not need the database?" Hint: consider what an attacker gains after exploiting the internet-facing component, and lateral movement.
Network Isolation
Why isolation matters. A single compromised container should not be a door into everything else on the host. Docker's networks provide boundaries, and you can tighten them further. Isolation is about reducing what can talk to what: between applications, between tiers, between containers and the internet, and between the network and published ports.
The isolation toolbox
| Mechanism | What it blocks | How |
|---|---|---|
| Separate user-defined networks | Traffic between unrelated applications | Docker drops forwarding between different bridge networks |
--internal network | Any traffic to or from outside the network; no default route, published ports unusable | docker network create --internal backend |
| Disable ICC on a network | Container-to-container traffic within the same network (published ports still work) | -o com.docker.network.bridge.enable_icc=false |
"icc": false in daemon.json | Container-to-container traffic on the default bridge | Daemon option |
| Bind published ports to localhost | Access to a published port from other machines | -p 127.0.0.1:5432:5432 |
DOCKER-USER rules | Arbitrary traffic patterns, e.g. by source network | iptables rules (see iptables and Docker) |
--network none | All network traffic | No interfaces besides loopback |
Analogy. Networks are separate floors of a building with no stairs between them. --internal is a floor with no exit to the street. Disabling ICC is putting each office on a floor behind a locked door. DOCKER-USER is the security desk checking visitors' badges.
Example: verifying each boundary
docker network create app-a
docker network create app-b
docker network create --internal secure
docker network create -o com.docker.network.bridge.enable_icc=false noicc
# 1. Separate networks cannot reach each other
docker run -d --name a1 --network app-a nginx:1.27-alpine
# "index" is required because Go templates do not accept "-" in field names
A1_IP=$(docker inspect -f '{{(index .NetworkSettings.Networks "app-a").IPAddress}}' a1)
docker run --rm --network app-b alpine wget -T 3 -qO- "http://$A1_IP" \
|| echo "blocked: different network"
# 2. An internal network has no route to the outside
docker run --rm --network secure alpine wget -T 3 -qO- http://example.com \
|| echo "blocked: internal network"
# 3. ICC disabled: neighbors on the same network cannot talk directly
docker run -d --name n1 --network noicc nginx:1.27-alpine
docker run --rm --network noicc alpine wget -T 3 -qO- http://n1 \
|| echo "blocked: inter-container communication disabled"
docker rm -f a1 n1 && docker network rm app-a app-b secure noicc
What network isolation does not do
- It does not encrypt traffic. Containers on the same network, and anyone with root on the host, can observe it; use TLS (or encrypted overlays across hosts).
- It does not authenticate. Anything on the network can connect to an open port; services still need credentials.
- It does not protect against a container with
--network host,NET_ADMIN, or access to the Docker socket, which can bypass or change the rules.
Recent Engines (28+) also harden the edges: by default, other machines on the LAN can no longer reach unpublished container ports by routing directly to container IPs, and ports published on 127.0.0.1 are no longer reachable from neighboring hosts.
Key takeaways
- Give each application, and each tier, its own network; Docker blocks traffic between networks.
- Use
--internalfor networks that must never reach or be reached from outside, such as databases. - Disable ICC where containers on a network should only be reached through published ports.
- Bind sensitive published ports to
127.0.0.1and addDOCKER-USERrules for finer control; isolation complements, but does not replace, TLS and authentication.
🧪 Practice
- (Beginner) Run the three verification tests above, then repeat test 2 after
docker network connect app-a <container>on the internal-network container. Explain why it can now reach the internet.- (Intermediate) Design and implement the networks for a stack of
proxy,web,worker,redis, andpostgres, where onlyproxyis public,workerhas no internet access, andredisandpostgresare reachable only by the services that use them. Document your network-to-container matrix.- Interview question: "Is a container on an
--internalnetwork fully safe from data exfiltration?" Hint: consider other containers on the same network that do have internet access, DNS, shared volumes, and what the host itself can see.
Troubleshooting Connectivity
Why a method matters. "It can't connect" has a dozen possible causes, from a typo in a hostname to an MTU mismatch on a VPN. Random changes waste hours. Work through the network layer by layer, from the inside out: is the service listening, can the client resolve the name, can it reach the IP, is the port published, and is a firewall interfering? Each step either finds the problem or rules out a whole class of causes.
1. Is the target running and LISTENING on 0.0.0.0:<container port>? -> ss -lntp in its namespace
|
2. Do client and target share a network? -> docker inspect / network inspect
|
3. Does the name resolve from the client? -> nslookup / getent hosts
|
4. Does a TCP connection to IP:port succeed? -> nc -zv / curl -v
|
5. From outside: is the port published and bound to the right IP? -> docker port / ss on host
|
6. Firewall, NAT, forwarding, MTU? -> iptables, sysctl, tcpdump
Toolbox
| Tool | Use |
|---|---|
nicolaka/netshoot | Image with curl, dig, nc, ss, tcpdump, iperf3, mtr, and more |
docker run --network container:<name> nicolaka/netshoot | Debug inside a target's namespace without adding tools to its image |
nsenter -t <pid> -n <cmd> | Run host binaries in a container's network namespace |
docker network inspect <net> | Which containers are attached, with which IPs |
docker port <container> | Actual host port bindings |
tcpdump -i docker0 / -i vethXXXX | See whether packets arrive and replies leave |
docker debug <container> | Debug shell for minimal images (Docker Desktop feature, see docker debug) |
Example: a systematic session
# Scenario: "api" cannot connect to "db:5432"
# 1. Is PostgreSQL listening, and on which address? Attach a toolbox to db's namespace
docker run --rm --network container:db nicolaka/netshoot ss -lntp
# LISTEN 0 244 0.0.0.0:5432 0.0.0.0:* <- good; "127.0.0.1:5432" would be the bug
# 2. Do they share a network?
docker inspect -f '{{range $n, $_ := .NetworkSettings.Networks}}{{$n}} {{end}}' api db
# appnet
# backend <- different networks: the bug in this scenario
# 3. Name resolution from the client's point of view
docker run --rm --network container:api nicolaka/netshoot dig +short db
# (empty) -> name not visible from api's networks
# Fix and re-test the TCP connection
docker network connect backend api
docker run --rm --network container:api nicolaka/netshoot nc -zv db 5432
# Connection to db (172.21.0.2) 5432 port [tcp/postgresql] succeeded!
# 5-6. Outside access problems: bindings, then packets on the bridge
docker port web
sudo tcpdump -ni docker0 tcp port 80 -c 10
Common symptoms
| Symptom | Likely cause | Check or fix |
|---|---|---|
bad address 'db' / Name or service not known | Not on a shared user-defined network, default bridge, typo, target not running | docker network inspect; use a user-defined network |
Connection refused | Nothing listening on that port: app bound to 127.0.0.1, wrong port (host port used instead of container port), app still starting | ss -lntp in the target namespace; use the container port between containers |
| Timeout | Isolation between networks, --internal, DOCKER-USER rules, ICC disabled, app hung | iptables -L DOCKER-USER -nv; check network options |
| Works from the host, not from other machines | Port bound to 127.0.0.1, cloud security group, upstream firewall | docker port; ss -lntp on the host |
| Container cannot reach the internet | ip_forward disabled, Docker rules flushed, internal network, upstream DNS unreachable, corporate proxy required | sysctl net.ipv4.ip_forward; restart dockerd; test ping 1.1.1.1 versus ping example.com |
| Small requests work, large transfers or TLS handshakes hang | MTU mismatch (VPN, overlay, cloud) | ping -M do -s 1472 <target>; lower the network MTU |
| Some internal hosts unreachable after Docker starts | Docker subnet overlaps a LAN or VPN route | ip route; change bip and default-address-pools |
localhost:5432 fails from another container | localhost is the container itself | Use the service or container name |
For daemon-level diagnostics and deeper packet analysis, see Inspecting Network Issues.
Key takeaways
- Diagnose from the inside out: listening socket, shared network, DNS, TCP reachability, publishing, firewall and MTU.
- Attach
nicolaka/netshootwith--network container:<name>to debug any container without modifying its image. Connection refusedmeans "reached the host, nothing listening"; a timeout points to filtering, isolation, or routing.- Between containers, use names and container ports; published host ports and
localhostare for the outside and the container itself.
🧪 Practice
- (Beginner) Start a Python HTTP server bound to
127.0.0.1inside a container on a user-defined network, try to reach it from another container, and usenetshootto diagnose and fix it.- (Intermediate) Ask a colleague (or write a script) to break a working two-container setup in one of three ways: different networks, a
DOCKER-USERdrop rule, or a lowered MTU on one network. Diagnose each case using only the step-by-step method, and note which step revealed the cause.- Interview question: "A containerized service intermittently fails to reach an external API with timeouts, while other containers on the same host are fine. How would you investigate?" Hint: compare the networks and DNS settings of the affected containers, capture traffic on the container's veth, and consider conntrack table exhaustion, source port exhaustion (SNAT), and DNS caching.
<a id="8-docker-compose"></a>