Networking is where most container confusion happens. This lesson demystifies how containers talk to each other and the outside world — from virtual bridges to DNS-based service discovery.
1. How Container Networking Works
Each container gets its own network namespace — an isolated copy of the network stack with its own interfaces, IP addresses, routing table, and iptables rules. From the container's perspective, it has a full network environment just like a standalone machine.
Docker creates virtual network infrastructure to connect these isolated namespaces together and to the outside world.
Think of each container as a computer on a virtual LAN. Docker acts as the network administrator — it creates virtual switches (bridges), assigns IP addresses (DHCP), configures routing, and sets up port forwarding (NAT) so the outside world can reach specific containers.
Key components of Docker networking:
- Network namespace — isolated network stack per container
- veth pair — virtual ethernet cable connecting container to bridge
- Bridge — virtual switch connecting containers together
- iptables/nftables — NAT and port forwarding rules
- Embedded DNS — name resolution between containers
2. Network Drivers
Docker uses a pluggable driver model. Each driver provides different connectivity and isolation characteristics:
| Driver | Description | Use Case |
|---|---|---|
bridge |
Virtual switch on single host; containers get private IPs | Default for standalone containers; most common for development |
host |
Container shares host's network namespace directly | Performance-critical apps; tools that need raw host network access |
none |
No networking; container only has loopback | Security-sensitive batch jobs that need no network |
overlay |
Multi-host networking via VXLAN tunnels | Docker Swarm / multi-node clusters |
macvlan |
Container gets a real MAC address on the physical network | Legacy apps that need to appear as physical devices on the LAN |
3. Bridge Network (Default)
When Docker starts, it creates a virtual bridge called docker0. Every container that uses the default bridge network gets:
- A veth pair — one end in the container (usually
eth0), one end attached todocker0 - A private IP address from the bridge's subnet (typically
172.17.0.0/16) - A default route via the bridge to reach the host and internet
On the default bridge network, containers can communicate by IP address but NOT by container name. There's no automatic DNS resolution. You must use user-defined bridge networks for name-based discovery.
# Inspect the default bridge
docker network inspect bridge
# See the veth pairs on the host
ip link show type veth
# Container's network view
docker exec mycontainer ip addr show eth0
4. Port Mapping
Containers have private IPs that are not reachable from outside the host. To expose a container's service to the outside world, you map a host port to a container port:
# Map host port 8080 → container port 80
docker run -d -p 8080:80 nginx
# Map on specific interface only
docker run -d -p 127.0.0.1:8080:80 nginx
# Map a range
docker run -d -p 8000-8010:8000-8010 myapp
# Let Docker pick a random host port
docker run -d -p 80 nginx
docker port <container> # shows assigned port
Under the hood, -p 8080:80 creates an iptables DNAT rule:
# Docker adds rules like this automatically:
iptables -t nat -A DOCKER -p tcp --dport 8080 \
-j DNAT --to-destination 172.17.0.2:80
-p HOST:CONTAINER — "left is the outside (host), right is the inside (container)." Same order as volume mounts (-v host:container).
5. User-Defined Bridge Networks
User-defined bridges are superior to the default bridge in almost every way:
| Feature | Default Bridge | User-Defined Bridge |
|---|---|---|
| DNS by container name | ❌ No | ✅ Yes |
| Automatic isolation | All containers share one bridge | Each network is isolated |
| Connect/disconnect at runtime | Must stop container | ✅ Hot-plug |
| Custom subnets | ❌ | ✅ --subnet |
# Create a user-defined bridge network
docker network create mynet
# Run containers on that network
docker run -d --name web --network mynet nginx
docker run -d --name api --network mynet node-app
# Containers can reach each other BY NAME
docker exec api ping web # resolves to web's IP!
docker exec api curl http://web # works!
# Connect a running container to another network
docker network connect mynet existing-container
# Disconnect
docker network disconnect mynet existing-container
# Inspect the network
docker network inspect mynet
# Clean up
docker network rm mynet
Multi-container communication on a user-defined network uses Docker's embedded DNS — no hardcoded IPs, no manual /etc/hosts editing, and containers can be replaced (new IP) without breaking connections.
6. Host Network
With --network host, the container shares the host's network namespace directly. There is no network isolation — the container sees all host interfaces, uses the host's IP, and binds directly to host ports.
# Container binds directly to host port 80
docker run -d --network host nginx
# No -p needed — nginx is listening on host:80 directly
curl http://localhost:80
Trade-offs:
- ✅ No NAT overhead — slightly better network performance
- ✅ Container can see all host traffic (useful for monitoring tools)
- ✅ No port mapping complexity
- ❌ No network isolation — container can bind any port
- ❌ Port conflicts — two containers can't both use port 80
- ❌ Only works on Linux (on Mac/Windows, Docker runs in a VM so "host" is the VM, not your machine)
Performance-critical applications where NAT overhead matters (high-throughput proxies, real-time streaming), or network tools that need to capture/inspect all host traffic (tcpdump, Prometheus node-exporter).
7. None Network
With --network none, the container gets complete network isolation. It only has a loopback interface (lo) — no external connectivity at all.
# Completely isolated container
docker run --network none alpine ip addr show
# Only shows: lo (127.0.0.1)
# No internet, no container-to-container communication
docker run --network none alpine ping 8.8.8.8
# ping: sendto: Network is unreachable
Use cases:
- Batch processing jobs that process local data and need no network
- Security-sensitive computation (cryptographic operations, secret generation)
- Testing application behavior when network is unavailable
8. DNS & Service Discovery
Docker runs an embedded DNS server at 127.0.0.11 inside every container on a user-defined network. This server resolves container names to their current IP addresses.
# Inside a container on a user-defined network:
cat /etc/resolv.conf
# nameserver 127.0.0.11
# Resolution happens by container name
nslookup web
# Address: 172.18.0.2
# Also works with network aliases
docker run -d --name db --network mynet --network-alias database postgres
# Both "db" and "database" resolve to this container
This is the foundation of microservice communication. Services refer to each other by name, not by IP:
# Application code connects to "redis" not "172.18.0.4"
redis.createClient({ host: 'redis', port: 6379 })
# If the redis container is replaced (new IP), the name still resolves
docker stop redis
docker run -d --name redis --network mynet redis:7
# Other containers automatically resolve the new IP
Just as a company's internal DNS lets you reach mail.internal without knowing the server's IP (and it still works when the server is migrated), Docker's embedded DNS lets containers find each other by name regardless of IP changes.
Knowledge Check
You run two containers on the default bridge network. Can container A reach container B by name (e.g., ping containerB)?
What does -p 3000:80 mean?
What is the main trade-off of using --network host?
Hands-On Tasks
Create a network, run two containers, and verify name resolution:
# 1. Create a user-defined bridge network
docker network create testnet
# 2. Run two containers on that network
docker run -d --name server1 --network testnet alpine sleep 3600
docker run -d --name server2 --network testnet alpine sleep 3600
# 3. Ping by name (should work!)
docker exec server1 ping -c 3 server2
# 4. Verify DNS resolution
docker exec server1 nslookup server2
# 5. Compare with default bridge — this will FAIL:
docker run -d --name server3 alpine sleep 3600
docker run -d --name server4 alpine sleep 3600
docker exec server3 ping -c 1 server4 # fails!
# 6. Clean up
docker stop server1 server2 server3 server4
docker rm server1 server2 server3 server4
docker network rm testnet
Compare the two approaches for exposing nginx:
# Approach A: Bridge with port mapping
docker run -d --name nginx-bridge -p 8080:80 nginx
curl http://localhost:8080 # works via iptables DNAT
# Check — container has its OWN IP:
docker exec nginx-bridge ip addr show eth0
# Approach B: Host network (Linux only)
docker run -d --name nginx-host --network host nginx
curl http://localhost:80 # direct, no NAT
# Check — container uses HOST's network:
docker exec nginx-host ip addr show
# You'll see the host's real interfaces!
# Compare performance (optional, run ab or wrk)
# Host networking has slightly lower latency for high-throughput
# Clean up
docker stop nginx-bridge nginx-host
docker rm nginx-bridge nginx-host
Podman uses CNI (Container Network Interface) or the newer Netavark for networking — same concepts (bridges, port mapping) but different plumbing. Kubernetes uses CNI plugins like Calico, Cilium, or Flannel — every pod gets an IP, and a flat network model means any pod can reach any other pod by IP (no NAT between pods). The concepts you learned here (namespaces, veth pairs, bridges, DNS discovery) apply everywhere.
Service meshes like Istio and Linkerd build on top of container networking. They inject sidecar proxy containers (Envoy) alongside your application containers. These proxies intercept all network traffic to provide mutual TLS, load balancing, retries, circuit breaking, and observability — all without changing your application code. The foundation is the same networking primitives: network namespaces, iptables rules, and DNS-based service discovery.
Key Takeaways
- Each container has its own network namespace — isolated interfaces, IPs, and routing
- Bridge is the default — containers get private IPs and connect via a virtual switch
- Port mapping (
-p HOST:CONTAINER) exposes container services externally via iptables DNAT - User-defined bridges are better — automatic DNS, better isolation, runtime connect/disconnect
- Host network = no isolation — container shares host's network stack, good for performance
- None network = full isolation — only loopback, for security-sensitive workloads
- Docker's embedded DNS (127.0.0.11) enables service discovery by container name on user-defined networks
- Never hardcode container IPs — use names and let DNS handle resolution