Back to Containers & Runtime Environments Mastery Series

Part 10: Docker Networking Fundamentals

May 14, 2026 Wasil Zafar 28 min read

Networking is where containers stop being isolated sandboxes and start becoming useful distributed systems. Understanding Docker's network model — from the default bridge to multi-host overlays — is the difference between "works on my machine" and "works in production." This article maps every network driver, port mapping strategy, and communication pattern you'll need.

Table of Contents

  1. Why Networking Is the Hardest Part
  2. Docker Network Drivers
  3. Bridge Networks
  4. User-Defined vs Default Bridge
  5. Host Networking
  6. Port Mapping Deep Dive
  7. Container-to-Container Communication
  8. Overlay Networks
  9. Macvlan Networks
  10. Network Inspection & Debugging
  11. Exercises
  12. Conclusion & Next Steps

Why Networking Is the Hardest Part

Ask any ops engineer what causes the most container outages, and the answer is almost always networking. Containers are designed for isolation — each one gets its own network namespace with its own interfaces, routing table, and iptables rules. That isolation is powerful for security but creates a fundamental tension: how do you connect things that are designed to be disconnected?

The Core Paradox: Containers achieve security through isolation, but applications achieve value through communication. Docker networking is the carefully controlled bridge between these two opposing forces.

The networking challenges compound at scale:

  • Single host — Multiple containers need to talk to each other and the outside world
  • Multi-host — Containers on different machines need seamless communication
  • Service discovery — Containers start and stop; IP addresses change constantly
  • Load balancing — Traffic needs distribution across container replicas
  • Security — Not every container should reach every other container

Docker provides five network drivers to address these scenarios, each with different trade-offs between isolation, performance, and complexity.

Docker Network Drivers Overview

Every Docker network is backed by a driver that determines how packets flow between containers, the host, and the external network. Understanding driver selection is the most impactful networking decision you'll make.

Driver Scope Isolation DNS Performance Use Case
bridge Single host Network namespace per container User-defined only Good (NAT overhead) Default for standalone containers
host Single host None — shares host network Host resolver Best (no encapsulation) Performance-critical apps, no port conflicts
overlay Multi-host VXLAN encapsulation Built-in service discovery Moderate (encapsulation cost) Swarm services, cross-host communication
macvlan Single host MAC per container on physical network None (external DNS) Excellent (no NAT) Legacy apps needing direct LAN presence
none Single host Complete (no networking) None N/A Batch processing, security-sensitive workloads

Decision Matrix: When to Use Each Driver

# List all networks on your Docker host
docker network ls

# Output:
# NETWORK ID     NAME      DRIVER    SCOPE
# a1b2c3d4e5f6   bridge    bridge    local
# f6e5d4c3b2a1   host      host      local
# 0a1b2c3d4e5f   none      null      local
Default Behavior: When you run docker run without specifying --network, the container joins the default bridge network. This is almost never what you want in production — always create and specify user-defined bridge networks.

Bridge Networks

The bridge driver is Docker's default and most commonly used network type. It creates a virtual Ethernet bridge (a software switch) on the host, and every container on that bridge gets its own IP address from a private subnet.

The Default docker0 Bridge

When Docker installs, it creates a bridge interface called docker0 with a default subnet (typically 172.17.0.0/16). Every container that doesn't specify a network connects here:

# Inspect the default bridge network
docker network inspect bridge

# Key output fields:
# "Subnet": "172.17.0.0/16"
# "Gateway": "172.17.0.1"

# See docker0 on the host
ip addr show docker0
# => docker0:  mtu 1500
# =>     inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0

# Run a container and check its IP
docker run --rm alpine ip addr show eth0
# => eth0: inet 172.17.0.2/16 scope global eth0

Creating Custom Bridge Networks

# Create a user-defined bridge network
docker network create --driver bridge \
  --subnet 192.168.100.0/24 \
  --gateway 192.168.100.1 \
  --ip-range 192.168.100.128/25 \
  my-app-network

# Run containers on the custom network
docker run -d --name web --network my-app-network nginx:alpine
docker run -d --name api --network my-app-network node:alpine

# Containers can reach each other by NAME (DNS)
docker exec web ping -c 3 api
# => PING api (192.168.100.129): 64 bytes from 192.168.100.129

# Connect a running container to an additional network
docker network connect my-app-network existing-container

# Disconnect a container from a network
docker network disconnect bridge existing-container
Bridge Network Architecture
flowchart TD
    subgraph Host["Docker Host"]
        subgraph Bridge["docker0 / user-defined bridge"]
            direction LR
            VETH1["veth pair"] --- C1["Container A
172.17.0.2"] VETH2["veth pair"] --- C2["Container B
172.17.0.3"] VETH3["veth pair"] --- C3["Container C
172.17.0.4"] end GW["Gateway 172.17.0.1"] IPT["iptables NAT"] end EXT["External Network / Internet"] Bridge --- GW GW --- IPT IPT --- EXT

User-Defined Bridge vs Default Bridge

This is one of Docker's most important networking distinctions. The default bridge network and user-defined bridge networks behave differently in critical ways:

Feature Default Bridge User-Defined Bridge
DNS resolution No — must use IP or --link (deprecated) Yes — automatic by container name
Isolation All containers share one network Separate network per logical group
Live connect/disconnect Requires restart Hot-plug without restart
Configuration Docker-managed (limited) Custom subnet, gateway, IP range
ICC (inter-container) All containers can reach all others Only containers on same network
Environment variables --link shares env vars Use DNS — no env var leaking
# Demonstrate DNS resolution difference

# Default bridge — no DNS
docker run -d --name db-default alpine sleep 3600
docker run --rm alpine ping -c 1 db-default
# => ping: bad address 'db-default'  (FAILS)

# User-defined bridge — DNS works
docker network create mynet
docker run -d --name db-custom --network mynet alpine sleep 3600
docker run --rm --network mynet alpine ping -c 1 db-custom
# => PING db-custom (192.168.100.2): 56 data bytes (SUCCESS)

# Cleanup
docker rm -f db-default db-custom
docker network rm mynet
Best Practice: Never use the default bridge network in production. Always create user-defined networks so containers can discover each other by name. The default bridge exists only for backward compatibility.

Host Networking

With --network host, Docker removes all network isolation. The container shares the host's network namespace directly — it sees the same interfaces, same IP addresses, and same ports as the host machine.

# Run nginx directly on host's port 80
docker run -d --network host --name web-host nginx:alpine

# No port mapping needed — nginx binds to host's port 80
curl http://localhost:80
# => Welcome to nginx!

# Container sees host's network interfaces
docker exec web-host ip addr
# => Shows eth0, wlan0, etc. — same as host

# Check — no docker-proxy process for port forwarding
ss -tlnp | grep 80
# => shows nginx directly bound, not docker-proxy
Aspect Bridge Network Host Network
Port mapping Required (-p) Not needed — direct bind
Performance NAT overhead (~5% latency) Native performance
Port conflicts Isolated per container Competes with host services
Security Network namespace isolation No isolation — full host access
Multiple replicas Different host ports per replica Cannot — port conflict
When to Use Host Networking: High-frequency trading, media streaming, or monitoring agents where the 1-5% NAT latency overhead matters, and you can guarantee no port conflicts. Always a trade-off: performance gains vs security loss.

Port Mapping Deep Dive

Port mapping is how containers on bridge networks expose services to the outside world. Docker uses iptables DNAT rules to forward host ports to container ports.

# Basic port mapping: hostPort:containerPort
docker run -d -p 8080:80 --name web nginx:alpine
# => Host:8080 forwards to Container:80

# Map to specific host IP (not all interfaces)
docker run -d -p 127.0.0.1:8080:80 --name web-local nginx:alpine
# => Only accessible from localhost, not external

# Map UDP port
docker run -d -p 5353:53/udp --name dns coredns/coredns

# Random host port (Docker picks available port)
docker run -d -P --name web-random nginx:alpine
docker port web-random
# => 80/tcp -> 0.0.0.0:32768

# Multiple port mappings
docker run -d \
  -p 80:80 \
  -p 443:443 \
  -p 8443:8443 \
  --name multi-port nginx:alpine

# Map a range of ports
docker run -d -p 7000-7010:7000-7010 --name port-range myapp

# Check which ports a container exposes
docker inspect --format='{{range $p, $conf := .NetworkSettings.Ports}}{{$p}} -> {{(index $conf 0).HostPort}}{{"\n"}}{{end}}' web
Under the Hood

How Port Mapping Works: iptables DNAT

When you publish a port with -p 8080:80, Docker creates an iptables rule that rewrites the destination of incoming packets. A packet arriving at host:8080 gets its destination changed to the container's IP:80 via DNAT (Destination NAT). Additionally, Docker starts a docker-proxy process for hairpin NAT scenarios where a container connects to its own published port via the host IP.

# See the iptables rules Docker creates
sudo iptables -t nat -L DOCKER -n -v
# => DNAT  tcp  --  0.0.0.0/0  0.0.0.0/0  tcp dpt:8080 to:172.17.0.2:80

# docker-proxy process
ps aux | grep 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
iptables DNAT docker-proxy

Container-to-Container Communication

Within the same user-defined bridge network, containers can freely communicate using container names as hostnames. Docker runs an embedded DNS server at 127.0.0.11 that resolves these names.

# Create a network and deploy a multi-container app
docker network create app-net

# Database
docker run -d --name postgres --network app-net \
  -e POSTGRES_PASSWORD=secret \
  postgres:16-alpine

# Application connects to database by name
docker run -d --name api --network app-net \
  -e DATABASE_URL="postgresql://postgres:secret@postgres:5432/mydb" \
  my-api-image

# Frontend connects to API by name
docker run -d --name frontend --network app-net \
  -p 3000:3000 \
  -e API_URL="http://api:8080" \
  my-frontend-image

# Verify connectivity
docker exec api ping -c 2 postgres
# => PING postgres (192.168.100.2): 56 data bytes — success

# DNS resolution from inside a container
docker exec api nslookup postgres
# => Name:    postgres
# => Address: 192.168.100.2

Cross-Network Isolation

# Containers on different networks CANNOT communicate
docker network create frontend-net
docker network create backend-net

docker run -d --name web --network frontend-net alpine sleep 3600
docker run -d --name db --network backend-net alpine sleep 3600

# This fails — networks are isolated
docker exec web ping -c 1 db
# => ping: bad address 'db'

# Connect the API to BOTH networks for controlled access
docker run -d --name api --network frontend-net alpine sleep 3600
docker network connect backend-net api

# Now api can reach both web and db
docker exec api ping -c 1 web   # => Success
docker exec api ping -c 1 db    # => Success

# But web still cannot reach db directly
docker exec web ping -c 1 db    # => Fails
Network Isolation with Multi-Homed Container
flowchart LR
    subgraph FN["frontend-net"]
        WEB["web"]
        API_F["api (eth0)"]
    end
    subgraph BN["backend-net"]
        API_B["api (eth1)"]
        DB["db"]
    end

    WEB <-->|"can communicate"| API_F
    API_B <-->|"can communicate"| DB
    API_F -.-|"same container"| API_B
    WEB x--x|"blocked"| DB
                            

Overlay Networks

Overlay networks solve the multi-host problem: how do containers on different physical machines communicate as if they're on the same LAN? The answer is VXLAN (Virtual eXtensible LAN) tunneling — wrapping container packets inside UDP packets that traverse the physical network.

# Initialize Docker Swarm (required for overlay networks)
docker swarm init --advertise-addr 192.168.1.10

# Create an overlay network
docker network create --driver overlay \
  --subnet 10.0.9.0/24 \
  --attachable \
  my-overlay

# --attachable allows standalone containers (not just services)

# Deploy a service across multiple nodes
docker service create --name web \
  --network my-overlay \
  --replicas 3 \
  --publish published=80,target=80 \
  nginx:alpine

# Containers on different hosts can communicate by service name
# Host A: container on 10.0.9.3
# Host B: container on 10.0.9.4
# They communicate as if on same LAN
Overlay Network — VXLAN Tunnel
flowchart TB
    subgraph H1["Host 1 (192.168.1.10)"]
        C1["Container A
10.0.9.3"] VTEP1["VXLAN Tunnel Endpoint"] end subgraph H2["Host 2 (192.168.1.11)"] C2["Container B
10.0.9.4"] VTEP2["VXLAN Tunnel Endpoint"] end PHYS["Physical Network
UDP:4789 encapsulation"] C1 --> VTEP1 VTEP1 -->|"VXLAN encap"| PHYS PHYS -->|"VXLAN decap"| VTEP2 VTEP2 --> C2
Key Point: Overlay networks add ~10-15% latency overhead due to VXLAN encapsulation/decapsulation. For latency-sensitive workloads across hosts, consider host networking with an external service mesh (like Cilium with eBPF) which can bypass kernel network stack entirely.

Macvlan Networks

Macvlan assigns a unique MAC address to each container and places them directly on the physical network. Containers appear as physical devices on the LAN — they get IPs from the same DHCP server as bare-metal machines.

# Create a macvlan network on the host's physical interface
docker network create -d macvlan \
  --subnet=192.168.1.0/24 \
  --gateway=192.168.1.1 \
  -o parent=eth0 \
  my-macvlan

# Run a container with a specific IP on the LAN
docker run -d --network my-macvlan \
  --ip 192.168.1.200 \
  --name legacy-app \
  my-legacy-image

# The container is reachable from any device on the LAN
# External machine:
ping 192.168.1.200
# => 64 bytes from 192.168.1.200 — reachable directly

# Check the container's MAC address
docker exec legacy-app ip link show eth0
# => link/ether 02:42:c0:a8:01:c8 (unique MAC on physical network)
Macvlan Mode Description Use Case
bridge (default) Containers can communicate with each other and external network Most common — direct LAN presence
802.1q trunk Sub-interfaces with VLAN tags Multi-tenant environments with VLAN segmentation
passthru Entire NIC dedicated to single container Maximum performance, single container per NIC
Limitation: With macvlan, the host cannot communicate with its own containers directly (Linux kernel restriction). You need a macvlan subinterface on the host or a separate bridge interface for host-to-container traffic.

Network Inspection & Debugging

When containers can't communicate, you need systematic debugging tools. Docker provides built-in inspection commands, and Linux utilities give you low-level visibility.

# List all networks
docker network ls
# => NETWORK ID     NAME          DRIVER    SCOPE

# Inspect a network — shows containers, subnet, gateway
docker network inspect my-app-network

# Find which networks a container belongs to
docker inspect --format='{{json .NetworkSettings.Networks}}' my-container | jq

# Get container's IP address
docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' web

# Test connectivity between containers
docker exec web ping -c 3 api
docker exec web wget -qO- http://api:8080/health

# DNS resolution debugging
docker exec web nslookup api
docker exec web cat /etc/resolv.conf
# => nameserver 127.0.0.11 (Docker's embedded DNS)

# Check exposed and published ports
docker port web
# => 80/tcp -> 0.0.0.0:8080

# Network-level statistics
docker stats --format "table {{.Name}}\t{{.NetIO}}" --no-stream

# Trace packet flow with tcpdump (from host)
docker run --rm --net=container:web nicolaka/netshoot tcpdump -i eth0 -n port 80
Debugging Toolkit

The nicolaka/netshoot Container

When your minimal containers (Alpine, distroless) lack debugging tools, use netshoot — a purpose-built networking troubleshooting container with curl, dig, nslookup, tcpdump, nmap, iperf3, and more. Attach it to any container's network namespace:

# Attach to a container's network namespace for debugging
docker run -it --rm --net=container:problematic-container \
  nicolaka/netshoot bash

# Now you have full networking tools in that container's network
curl http://localhost:8080/health
dig api.my-network
ss -tlnp
traceroute external-service.com
netshoot tcpdump debug

Exercises

Exercise 1

Multi-Container Application Network

Create a user-defined bridge network. Deploy PostgreSQL, a Node.js API that connects to Postgres by name, and an Nginx reverse proxy that forwards to the API by name. Verify the full chain works without using any IP addresses — only container names.

Exercise 2

Network Isolation Test

Create three networks: frontend, backend, and monitoring. Deploy containers on each and verify that containers on frontend cannot reach backend containers directly. Then create a "gateway" container connected to both and verify it can reach containers on both networks.

Exercise 3

Host vs Bridge Benchmark

Run iperf3 in server mode on a container with bridge networking, then host networking. Compare the bandwidth and latency results. Use docker run --rm -it --network host nicolaka/netshoot iperf3 -c localhost for host mode. Measure at least 10 runs and calculate the average difference.

Exercise 4

Port Mapping Exploration

Run 5 Nginx containers with -P (random ports). Use docker port to discover assigned ports. Then use sudo iptables -t nat -L DOCKER -n to see the iptables rules Docker created. Map the relationship between each rule and each container.

Conclusion & Next Steps

Docker networking is fundamentally about controlling connectivity — who can talk to whom, how packets flow, and where services are discovered. The key takeaways:

  • Always use user-defined bridges — automatic DNS, isolation, live connect/disconnect
  • Bridge for single-host apps — the right default for 90% of workloads
  • Host for performance — when NAT overhead is unacceptable, accept the security trade-off
  • Overlay for multi-host — VXLAN tunneling makes containers on different hosts appear local
  • Macvlan for legacy — when applications must appear directly on the physical network
  • Port mapping = iptables — understanding DNAT helps debug connectivity issues

With these fundamentals solid, you're ready to go deeper into the Linux primitives that make all of this work — veth pairs, iptables rules, network namespaces, and the DNS server that ties it all together.

Next in the Series

In Part 11: Advanced Networking & Service Discovery, we dive beneath Docker's abstractions into the Linux networking primitives — veth pairs, bridges, iptables chains, NAT traversal, and the embedded DNS server — giving you the debugging superpowers to diagnose any networking issue.