Skip to main content

Docker Networking Deep Dive — Bridge, Host, Overlay, and None

Master Docker's four network drivers and understand when to use each for local development, single-host production, and multi-host cluster deployments.

44 Terms

Overview and What You Will Learn

When a container tries to reach another container and fails, most engineers guess at the solution — restart things, change ports, add flags at random. Engineers who understand Docker networking diagnose the same problem in 60 seconds because they know exactly which layer failed.

In this guide you will learn how Docker's four network drivers work, the critical difference between the default bridge network and user-defined bridge networks, why containers on different networks cannot talk to each other, and how to debug every common networking failure.

Core Principles

◈ DIAGRAM
+------------------------------------------+
| Docker Network Drivers |
| |
| bridge — default, single-host |
| containers get private IPs |
| user-defined = automatic DNS |
| |
| host — no isolation at all |
| container uses host network |
| best performance, worst safety |
| |
| overlay — multi-host communication |
| spans multiple Docker hosts |
| requires Docker Swarm |
| |
| none — no networking at all |
| complete network isolation |
| for batch jobs, data processing|
+------------------------------------------+

Detailed Step-by-Step Practical Lab

Milestone 1: The Default Bridge Network and Its Problems

When you run a container without specifying a network, it joins the default bridge network (docker0). This network has a critical limitation: no DNS. Containers can only reach each other by IP address, which changes every time a container restarts.

Bash
docker run -d --name web nginx
docker run -d --name db postgres:15 -e POSTGRES_PASSWORD=secret
# Get the IP of the db container
docker inspect db --format '{{.NetworkSettings.IPAddress}}'
# 172.17.0.3
# Try to ping by name from web container — FAILS
docker exec web ping db
# ping: bad address 'db'
# Default bridge has NO DNS — names do not resolve
# Must use IP address instead
docker exec web ping 172.17.0.3
# PING 172.17.0.3 — works, but this IP will change on restart
# This is why the default bridge network is unusable for real applications
# If db restarts, it gets a new IP, and web cannot find it anymore
Milestone 2: User-Defined Bridge Networks — The Correct Approach

User-defined bridge networks automatically resolve container names as DNS hostnames. This is how production multi-container applications work.

Bash
# Create a user-defined bridge network
docker network create payment-network
# Run containers on the user-defined network
docker run -d \
--name payment-api \
--network payment-network \
registry.razorpay.in/payment-api:v3.1.0
docker run -d \
--name postgres \
--network payment-network \
-e POSTGRES_PASSWORD=secret \
postgres:15
# Now ping by name — WORKS
docker exec payment-api ping postgres
# PING postgres (172.18.0.3): 56 bytes sent
# 64 bytes from 172.18.0.3: seq=0 ttl=64 time=0.124 ms
# Also works with the full container name
docker exec payment-api curl http://postgres:5432
# DNS is provided by Docker's embedded DNS server at 127.0.0.11
docker exec payment-api cat /etc/resolv.conf
# nameserver 127.0.0.11
# options ndots:0
# List all networks
docker network ls
# NETWORK ID NAME DRIVER SCOPE
# a84f9c2b1d3e bridge bridge local <- default
# b72c8a9f4e1d payment-network bridge local <- user-defined
# c91d8b3f2a5e host host local
# d03e5f4a6b7c none null local
# Inspect a network
docker network inspect payment-network
# Shows: subnet, gateway, connected containers and their IPs
# Connect an existing container to an additional network
docker network connect monitoring-network payment-api
# payment-api is now on both payment-network and monitoring-network
# Can communicate with containers on either network
# Disconnect from a network
docker network disconnect payment-network old-container
Milestone 3: Port Publishing — Making Containers Accessible
Bash
# Map host port 8080 to container port 80
docker run -d -p 8080:80 nginx
# Access via: curl http://localhost:8080
# Map to a specific host interface (not all interfaces)
docker run -d -p 127.0.0.1:8080:80 nginx
# Only accessible from localhost — not from other machines
# More secure for admin interfaces that should not be public
# Random host port (Docker chooses)
docker run -d -P nginx
# Docker picks a random high port (e.g., 0.0.0.0:32768->80/tcp)
docker port nginx-container-name
# 80/tcp -> 0.0.0.0:32768
# Map multiple ports
docker run -d \
-p 80:80 \
-p 443:443 \
nginx
# Check which port a container published
docker inspect payment-api \
--format '{{json .NetworkSettings.Ports}}'
# {"8080/tcp":[{"HostIp":"0.0.0.0","HostPort":"8080"}]}

Behind the scenes, Docker creates iptables DNAT rules:

Bash
# See the iptables rules Docker created for port publishing
sudo iptables -t nat -L DOCKER --line-numbers
# target prot opt source destination
# DNAT tcp -- anywhere anywhere tcp dpt:8080 to:172.17.0.2:80
# This redirects host port 8080 traffic to container IP:80
Milestone 4: Host Network Mode
Bash
# Run with host networking — no isolation, no NAT, no port mapping
docker run -d --network host nginx
# nginx now listens directly on host port 80
# No -p flag needed — and -p flags are IGNORED in host mode
# Check: nginx is listening on the host directly
ss -tulpn | grep :80
# tcp LISTEN 0 128 0.0.0.0:80 users:(("nginx",pid=12345,fd=6))
# When to use host network:
# 1. Monitoring agents that need to see host network traffic
# 2. Performance-critical services where NAT overhead matters
# 3. Tools that need to manage the host network (like Cilium CNI)
# When NOT to use:
# 1. Most application services (port conflicts with host services)
# 2. Any service that should be isolated from host network
# 3. Production workloads where isolation matters (security)
# Note: host networking is Linux-only
# Docker Desktop for macOS/Windows runs inside a VM
# --network host means host of that VM, not your Mac/Windows host
Milestone 5: Network Troubleshooting
Bash
# Inspect a container's network config
docker inspect container-name \
--format '{{json .NetworkSettings.Networks}}' | jq
# Check if two containers are on the same network
docker inspect api --format '{{json .NetworkSettings.Networks}}' | \
jq 'keys'
# ["payment-network"]
docker inspect db --format '{{json .NetworkSettings.Networks}}' | \
jq 'keys'
# ["monitoring-network"]
# Different networks! They cannot communicate.
# Fix: connect both to the same network
docker network connect payment-network db
# Test connectivity from inside a container
docker exec -it api sh
# Inside container:
ping db # Does DNS resolve?
nc -zv db 5432 # Is port 5432 open?
curl http://db:5432 # Can we make HTTP connection?
nslookup db # What IP does db resolve to?
cat /etc/resolv.conf # What DNS server is configured?
# Check iptables if containers cannot communicate even on same network
sudo iptables -L DOCKER-ISOLATION-STAGE-1
sudo iptables -L FORWARD
# Docker adds isolation rules — verify they are not blocking the traffic

Common Mistakes

Mistake Consequence Fix
Using default bridge network Container names do not resolve as DNS Create user-defined network: docker network create
Containers on different networks Cannot communicate at all Connect both to same network
Using host network in production Port conflicts, no isolation Use bridge with port publishing instead
Exposing all ports with -P Random ports, hard to manage Always specify exact ports: -p 8080:80
Binding to 0.0.0.0 for admin tools Admin interface accessible from internet Bind to 127.0.0.1:port:port for localhost-only services
Tip

When containers cannot reach each other, always check docker inspect container --format '{{json .NetworkSettings.Networks}}' for both containers first. The most common cause of container communication failures is simply that the containers are on different networks.

Remember

User-defined bridge networks provide automatic DNS. The default bridge network does not. This single difference is why you should always create a user-defined network for any multi-container application — never rely on the default bridge.

Common Mistake

Hardcoding container IP addresses in application configuration. Container IPs change every time a container restarts. Always use container names (on user-defined networks) or service names (in Docker Compose) — these are stable DNS names that always resolve to the current IP regardless of restarts.


JSON
{
"title": "Docker Volumes and Persistent Storage — Volumes, Bind Mounts, and tmpfs",
"slug": "docker-volumes-persistent-storage",
"cluster": "docker",
"description": "Manage persistent data in Docker using named volumes, bind mounts, and tmpfs — understanding the trade-offs and correct use case for each storage type.",
"primaryKeyword": "docker volumes"
}

Docker Volumes and Persistent Storage — Volumes, Bind Mounts, and tmpfs

Overview and What You Will Learn

By default, everything written inside a Docker container is lost when the container is removed. For stateless applications — web servers, API servers — this is fine. For databases, file uploads, cache data, or any information that must survive a container restart, you need persistent storage.

Docker provides three storage types: named volumes (Docker-managed, production-ready), bind mounts (host path mapped into container, development-ready), and tmpfs (in-memory, temporary). Each has a specific use case. Using the wrong one causes either data loss or poor performance.

Core Principles

Bash
+------------------------------------------+
| Named Volume |
| Managed by Docker |
| Stored at /var/lib/docker/volumes/ |
| Survives container removal |
| Portable between containers |
| Production databases, persistent data |
+------------------------------------------+
+------------------------------------------+
| Bind Mount |
| Host path mapped into container |
| /home/user/code -> /app/code |
| Source changes = live in container |
| Development hot reloading |
| Config file injection |
+------------------------------------------+
+------------------------------------------+
| tmpfs Mount |
| In-memory only (RAM) |
| Fastest possible I/O |
| Lost on container stop |
| Sensitive temp files, caches |
+------------------------------------------+

Detailed Step-by-Step Practical Lab

Milestone 1: Named Volumes
Bash
# Create a named volume
docker volume create postgres-data
# Inspect the volume
docker volume inspect postgres-data
# [{
# "Name": "postgres-data",
# "Driver": "local",
# "Mountpoint": "/var/lib/docker/volumes/postgres-data/_data",
# "Labels": {},
# "Scope": "local"
# }]
# Run PostgreSQL with a named volume
docker run -d \
--name postgres \
-e POSTGRES_PASSWORD=secret \
-v postgres-data:/var/lib/postgresql/data \
postgres:15
# Create a database table
docker exec -it postgres psql -U postgres -c \
"CREATE TABLE payments (id SERIAL, amount NUMERIC);"
# Remove the container
docker rm -f postgres
# Volume still exists with your data
docker volume ls
# DRIVER VOLUME NAME
# local postgres-data
# Start a new container with the same volume
docker run -d \
--name postgres-new \
-e POSTGRES_PASSWORD=secret \
-v postgres-data:/var/lib/postgresql/data \
postgres:15
# Data is still there
docker exec -it postgres-new psql -U postgres -c \
"SELECT * FROM payments;"
# Data persisted through container removal and recreation
# List all volumes
docker volume ls
# Remove a volume (only when you want to delete the data permanently)
docker volume rm postgres-data
# Error: volume is in use — must stop the container first
# Remove all unused volumes (volumes not attached to any container)
docker volume prune
# WARNING: This will remove all local volumes not used by containers
# This is permanent — deleted data cannot be recovered
Milestone 2: Bind Mounts
Bash
# Mount a host directory into a container
# Great for development — edit code on host, runs in container
docker run -d \
--name dev-api \
-p 3000:3000 \
-v $(pwd)/src:/app/src \
node:20-alpine \
node /app/src/server.js
# Changes to ./src/server.js on the host are immediately visible
# in the container — no rebuild needed
# Mount a single file (config injection)
docker run -d \
--name nginx \
-p 80:80 \
-v $(pwd)/nginx.conf:/etc/nginx/nginx.conf:ro \
nginx:1.25
# :ro = read-only — container cannot modify the host file
# Mount for log collection
docker run -d \
--name app \
-v /var/log/myapp:/app/logs \
myapp:latest
# Logs written to /app/logs in container appear at /var/log/myapp on host
# Useful for log shippers that read from the host filesystem
# Check current mounts on a container
docker inspect app --format '{{json .Mounts}}' | jq
# [{
# "Type": "bind",
# "Source": "/var/log/myapp",
# "Destination": "/app/logs",
# "Mode": "rw",
# "RW": true
# }]
Milestone 3: tmpfs Mounts
Bash
# Mount tmpfs (in-memory storage)
docker run -d \
--name secure-app \
--tmpfs /tmp \
--tmpfs /run \
myapp:latest
# /tmp and /run are in-memory
# Files written there never touch disk
# Lost when container stops
# tmpfs with size and mode limits
docker run -d \
--name api \
--mount type=tmpfs,destination=/tmp,tmpfs-size=100m,tmpfs-mode=1777 \
myapp:latest
# Limits tmpfs to 100MB
# Mode 1777 = sticky bit (like /tmp on Linux)
# Use case: sensitive temporary files that should never be on disk
# API tokens written to tmpfs during request processing
# Decrypted credentials that must not persist
# High-speed caches that do not need persistence
Milestone 4: Volumes in Docker Compose
YAML
# docker-compose.yml
version: "3.8"
services:
postgres:
image: postgres:15
environment:
POSTGRES_PASSWORD: secret
volumes:
# Named volume — managed by Docker
- postgres-data:/var/lib/postgresql/data
api:
image: registry.razorpay.in/payment-api:v3.1.0
volumes:
# Bind mount for config
- ./config/production.json:/app/config/production.json:ro
# tmpfs for temp files
- type: tmpfs
target: /tmp
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
# Declare named volumes at the top level
volumes:
postgres-data: # Docker creates and manages this
redis-data:
Bash
# Start with compose
docker compose up -d
# Volumes are created automatically if declared in the volumes section
docker volume ls
# local projectname_postgres-data
# local projectname_redis-data
# Data persists through docker compose down
docker compose down
# Containers removed, networks removed, volumes KEPT
# To also remove volumes (delete all data):
docker compose down -v
# WARNING: This deletes all data in all volumes
Milestone 5: Backup and Restore Volumes
Bash
# Backup a named volume to a tar file
docker run --rm \
-v postgres-data:/source:ro \
-v $(pwd):/backup \
alpine \
tar czf /backup/postgres-backup.tar.gz -C /source .
# Runs an alpine container that mounts the volume and your current dir
# Creates postgres-backup.tar.gz in your current directory
# Restore a volume from backup
docker volume create postgres-data-restored
docker run --rm \
-v postgres-data-restored:/target \
-v $(pwd):/backup:ro \
alpine \
tar xzf /backup/postgres-backup.tar.gz -C /target
# Extracts the backup into the new volume
# Verify restore worked
docker run --rm \
-v postgres-data-restored:/data \
postgres:15 \
ls /data
# Should show PostgreSQL data files

When to Use Each Storage Type

Use Case Storage Type Why
PostgreSQL data Named volume Persists, Docker-managed, easy backup
Redis data Named volume Persists through restarts
Dev code hot-reload Bind mount Host edits immediately visible in container
Nginx config injection Bind mount (read-only) Use host config file without rebuilding
Sensitive temp files tmpfs Never written to disk
High-speed cache tmpfs Fastest I/O, no persistence needed
Log files for shipper Bind mount Log shipper on host reads from host path

Common Mistakes

Mistake Consequence Fix
Storing DB data in container filesystem Data lost on container remove Use named volume: -v pgdata:/var/lib/postgresql/data
Using bind mount in production Host path must exist and have correct permissions Use named volumes in production, bind mounts in development only
Running docker compose down -v carelessly All volume data permanently deleted Never use -v unless you want to destroy all data
Not setting volume permissions Container cannot write to volume (permission denied) Set correct ownership in Dockerfile or entrypoint script
Forgetting to declare volumes in compose Volume created with random name, hard to manage Always declare named volumes in the volumes: top-level key
Tip

Use docker volume inspect volume-name to find the actual path on the host where volume data is stored (/var/lib/docker/volumes/name/_data). You can inspect or back up this directory directly, but always do so while the container is stopped to avoid data corruption.

Remember

docker compose down removes containers and networks but keeps volumes. docker compose down -v removes everything including volumes and all data inside them. The -v flag has caused many accidental data losses. Know exactly which command you are running before pressing Enter.

Common Mistake

Using bind mounts to map the entire application directory in production. Bind mounts in production mean the host filesystem must match the container's expectations exactly — a host OS upgrade, a file permission change, or a disk mount change can break the container. Use named volumes for data that must persist, and build everything else into the image.

Resources

Explore More in Docker Networking and Storage

All 3 Topics

Frequently Asked Questions

Is Docker Networking Deep Dive — Bridge, Host, Overlay, and None free to learn on DevOps Network?

Yes - this topic, like everything on DevOps Network, is 100% free with no paywall or sign-up gate.

What does the Docker Networking Deep Dive — Bridge, Host, Overlay, and None topic cover?

Master Docker's four network drivers and understand when to use each for local development, single-host production, and multi-host cluster deployments.