Every container you run participates in a network. Whether it is a single container on your laptop or a hundred services spread across a Swarm cluster, Docker networking determines which containers can talk to each other, how they discover services, and how traffic reaches them from the outside world. Getting networking wrong leads to mysterious connection timeouts, DNS failures, and containers that silently cannot reach each other.
This guide covers every Docker network driver, DNS service discovery, port mapping, Compose networking patterns, and practical debugging techniques. For the broader Docker picture, see The Complete Guide to Docker.
The bridge driver is Docker's default and most commonly used network type. When you install Docker, it creates a default bridge network called bridge (backed by the docker0 Linux bridge interface). Every container that starts without an explicit --network flag joins this default bridge.
The default bridge and user-defined bridges behave very differently:
# Container on the default bridge -- no automatic DNS
docker run -d --name web nginx
docker run -d --name app alpine sleep 3600
docker exec app ping web
# ping: bad address 'web' -- name resolution fails
# Container on a user-defined bridge -- automatic DNS works
docker network create my-net
docker run -d --name web2 --network my-net nginx
docker run -d --name app2 --network my-net alpine sleep 3600
docker exec app2 ping web2
# PING web2 (172.18.0.2): 56 data bytes -- works!
Key differences between default and user-defined bridges:
| Feature | Default bridge | User-defined bridge |
|---|---|---|
| Automatic DNS | No (IP only) | Yes (container names) |
| Container isolation | All containers see each other | Only containers on same network |
| Live connect/disconnect | Must recreate container | docker network connect/disconnect |
| Network aliases | Not supported | Supported (--network-alias) |
| Link support | Legacy --link only | Not needed (DNS works) |
Rule of thumb: always create user-defined bridge networks. The default bridge exists for backward compatibility and lacks the features you need in production.
Docker assigns subnets automatically, but you can control them explicitly. This is important when Docker's default ranges conflict with your corporate network or VPN:
# Create a bridge with a specific subnet
docker network create \
--driver bridge \
--subnet 10.10.0.0/24 \
--gateway 10.10.0.1 \
--ip-range 10.10.0.128/25 \
my-custom-net
# Assign a static IP to a container
docker run -d --name db \
--network my-custom-net \
--ip 10.10.0.10 \
postgres:16
Use the CIDR Calculator to plan your subnet ranges and avoid overlaps with existing infrastructure.
docker network inspect my-net
[
{
"Name": "my-net",
"Driver": "bridge",
"IPAM": {
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1"
}
]
},
"Containers": {
"a1b2c3...": {
"Name": "web2",
"IPv4Address": "172.18.0.2/16"
},
"d4e5f6...": {
"Name": "app2",
"IPv4Address": "172.18.0.3/16"
}
}
}
]
The host network driver removes network isolation between the container and the Docker host. The container shares the host's network namespace directly -- no virtual bridge, no NAT, no port mapping.
# Container binds directly to host port 80
docker run -d --network host nginx
# nginx is now accessible on host:80 with zero overhead
Host networking removes a significant isolation boundary. The container can bind to any port on the host, see all host network traffic, and access services listening on localhost. Do not use it for untrusted workloads. In production, prefer bridge networks with explicit port mapping unless you have measured a performance need for host networking.
Note:
--network hostonly works on Linux. On Docker Desktop (macOS/Windows), it connects to the VM's network, not the physical host.
Overlay networks enable communication between containers running on different Docker hosts. They are the foundation of Docker Swarm multi-host networking and also work with standalone containers when connected to Swarm.
# Initialize Swarm (required for overlay)
docker swarm init
# Create an overlay network
docker network create \
--driver overlay \
--attachable \
my-overlay
# Deploy a service on the overlay
docker service create \
--name api \
--network my-overlay \
--replicas 3 \
my-api:latest
Overlay networks use VXLAN encapsulation to tunnel Layer 2 frames inside Layer 3 UDP packets (port 4789). Each node participating in the overlay gets a VTEP (VXLAN Tunnel Endpoint). The Swarm control plane distributes network state to all nodes, so containers on node-1 can reach containers on node-3 by name -- transparently.
Required ports between Swarm nodes:
By default, overlay data traffic is unencrypted. For sensitive communication between nodes, enable IPsec encryption:
docker network create \
--driver overlay \
--opt encrypted \
secure-overlay
This adds ~10-15% overhead due to IPsec encapsulation. Use it for inter-node traffic crossing untrusted networks; skip it for nodes on the same trusted LAN.
By default, overlay networks only accept Swarm services. The --attachable flag allows standalone containers (docker run) to join the overlay, which is useful for debugging:
# Attach a debug container to the overlay
docker run -it --rm --network my-overlay alpine sh
# Now you can ping/curl Swarm services by name
Macvlan gives each container a MAC address on the physical network. The container appears as a physical device to the network -- it gets an IP from your LAN's DHCP server or a static IP in the same subnet as the host.
# Create a macvlan network on the host's eth0
docker network create \
--driver macvlan \
--subnet 192.168.1.0/24 \
--gateway 192.168.1.1 \
-o parent=eth0 \
my-macvlan
# Container gets a real LAN IP
docker run -d --name legacy-app \
--network my-macvlan \
--ip 192.168.1.50 \
my-legacy-app
Macvlan supports 802.1Q VLAN tagging by specifying a sub-interface:
# Container traffic tagged as VLAN 100
docker network create \
--driver macvlan \
--subnet 10.0.100.0/24 \
--gateway 10.0.100.1 \
-o parent=eth0.100 \
vlan100-net
Limitation: by default, the Docker host cannot communicate with macvlan containers. This is a Linux kernel restriction. The workaround is creating a macvlan interface on the host itself or using a separate bridge for host-to-container traffic.
Docker runs an embedded DNS server at 127.0.0.11 inside every container on user-defined networks. This is the mechanism that makes container name resolution work.
When container app runs ping web, the resolution path is:
app queries 127.0.0.11 (Docker's embedded DNS)web in the network's container registry# Verify DNS is configured inside a container
docker run --rm --network my-net alpine cat /etc/resolv.conf
# nameserver 127.0.0.11
# options ndots:0
A container can have multiple DNS names using --network-alias:
docker run -d \
--name postgres-primary \
--network my-net \
--network-alias db \
--network-alias postgres \
postgres:16
# All three names resolve to the same container:
# postgres-primary, db, postgres
Multiple containers can share the same alias. Docker round-robins DNS responses between them -- a basic form of load balancing:
docker run -d --name web1 --network my-net --network-alias web nginx
docker run -d --name web2 --network my-net --network-alias web nginx
docker run -d --name web3 --network my-net --network-alias web nginx
# "web" resolves to all three IPs (round-robin)
docker run --rm --network my-net alpine nslookup web
Override Docker's DNS forwarding with --dns:
docker run -d --name app \
--network my-net \
--dns 8.8.8.8 \
--dns 1.1.1.1 \
--dns-search example.com \
my-app
This tells the embedded DNS to forward unresolved queries to 8.8.8.8 and 1.1.1.1 instead of the host's DNS. The --dns-search flag sets the search domain, so ping api resolves as api.example.com.
Port mapping connects the outside world to containers. Docker uses iptables (or nftables) rules to NAT traffic from a host port to a container port.
# Map host port 8080 to container port 80
docker run -d -p 8080:80 nginx
# Map to a specific host interface (recommended for security)
docker run -d -p 127.0.0.1:8080:80 nginx
# Let Docker choose a random host port
docker run -d -p 80 nginx
docker port <container_id>
# 0.0.0.0:32768->80/tcp
# UDP port mapping
docker run -d -p 5353:53/udp my-dns
# Multiple port mappings
docker run -d -p 80:80 -p 443:443 nginx
By default, -p 8080:80 binds to 0.0.0.0 -- all interfaces. This means the port is accessible from the network, which is often not what you want for internal services:
# Accessible only from localhost
docker run -d -p 127.0.0.1:3000:3000 my-app
# Accessible only on a specific interface
docker run -d -p 10.0.0.5:8080:80 nginx
Security tip: always bind internal services (databases, admin panels, monitoring) to
127.0.0.1. Use the Firewall Rule Generator to create iptables/nftables rules that restrict container port access.
Port mapping is only relevant for traffic entering from outside the Docker network. Containers on the same user-defined network communicate directly on any port without mapping:
# No -p needed for container-to-container communication
docker run -d --name db --network my-net postgres:16
docker run -d --name app --network my-net my-app
# app can reach db on port 5432 without any -p flag
The -p flag is for external access only. This is a common source of confusion.
Docker Compose creates a default network for each project automatically. Every service in the Compose file joins this network and is reachable by its service name.
# docker-compose.yml
services:
web:
image: nginx
ports:
- "80:80"
api:
image: my-api
db:
image: postgres:16
Running docker compose up in a directory called myproject creates a network named myproject_default. All three services join it. api can reach db at db:5432, and web can reach api at api:3000 -- no extra configuration needed.
In production stacks, you typically want to isolate frontend from backend services:
services:
web:
image: nginx
ports:
- "80:80"
networks:
- frontend
api:
image: my-api
networks:
- frontend
- backend
db:
image: postgres:16
networks:
- backend
redis:
image: redis:7
networks:
- backend
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # no external access
In this layout, web can reach api but cannot reach db or redis directly. The api service bridges both networks. The internal: true flag on the backend network means containers on it cannot reach the internet -- useful for databases that should never make outbound connections.
Use the Docker Run to Compose Converter to translate docker run commands with network flags into Compose format.
To connect Compose services to a network managed outside the Compose file (e.g., shared between multiple stacks):
services:
app:
image: my-app
networks:
- shared
networks:
shared:
external: true
name: my-shared-net
The network must exist before you run docker compose up. Create it with docker network create my-shared-net.
networks:
app-net:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/24
gateway: 172.28.0.1
When containers cannot reach each other, a systematic approach saves hours of guessing.
# List all networks
docker network ls
# Check which containers are on a network
docker network inspect my-net --format '{{range .Containers}}{{.Name}} {{.IPv4Address}}{{println}}{{end}}'
# Check which networks a container belongs to
docker inspect app --format '{{json .NetworkSettings.Networks}}' | jq .
# Execute into the container
docker exec -it app sh
# Test DNS resolution
nslookup db
# or
getent hosts db
# Test TCP connectivity
nc -zv db 5432
# or use wget if nc is not available
wget -qO- --timeout=3 http://api:3000/health
If the container lacks networking tools, run a debug sidecar on the same network:
docker run -it --rm --network my-net nicolaka/netshoot
# netshoot includes: curl, dig, nslookup, tcpdump, iperf, nmap, ss, ip, etc.
# Capture traffic on the Docker bridge
sudo tcpdump -i br-$(docker network inspect my-net -f '{{.Id}}' | cut -c1-12) -n
# Capture traffic inside a container using nsenter
PID=$(docker inspect -f '{{.State.Pid}}' app)
sudo nsenter -t $PID -n tcpdump -i eth0 -n port 5432
# Or use a sidecar
docker run --rm --net=container:app nicolaka/netshoot tcpdump -i eth0 -n
Docker manipulates iptables heavily. If connectivity is broken after firewall changes, check the Docker chains:
# List Docker's NAT rules (port mapping)
sudo iptables -t nat -L DOCKER -n --line-numbers
# List Docker's filter rules (inter-container communication)
sudo iptables -L DOCKER -n --line-numbers
# Check DOCKER-USER chain (your custom rules go here)
sudo iptables -L DOCKER-USER -n --line-numbers
Important: never insert rules directly into the DOCKER chain -- Docker manages it. Use the DOCKER-USER chain for custom firewall rules. The Firewall Rule Generator can help you build the correct iptables rules.
| Symptom | Likely Cause | Fix |
|---|---|---|
| Container cannot resolve other container names | Using default bridge | Switch to a user-defined network |
| Connection refused between containers | Containers on different networks | Connect both to the same network |
| Port accessible on LAN but should not be | Bound to 0.0.0.0 | Bind to 127.0.0.1 explicitly |
| DNS works but connection times out | Firewall or iptables rules | Check DOCKER-USER chain |
| Overlay network containers cannot communicate | Swarm ports blocked | Open TCP/UDP 7946 and UDP 4789 |
| Container cannot reach the internet | internal: true network or DNS misconfigured | Check network settings and /etc/resolv.conf |
For DNS-specific issues, the DNS Resolution Failure runbook provides a step-by-step diagnostic checklist.
Docker networking is a layered system with a driver for every use case:
The most important practical rules:
For hands-on practice:
docker network and docker run --network commands interactivelydocker run flags and Compose YAML