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
+------------------------------------------+| 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.
# Run two containers on the default bridge networkdocker run -d --name web nginxdocker run -d --name db postgres:15 -e POSTGRES_PASSWORD=secret # Get the IP of the db containerdocker inspect db --format '{{.NetworkSettings.IPAddress}}'# 172.17.0.3 # Try to ping by name from web container — FAILSdocker exec web ping db# ping: bad address 'db'# Default bridge has NO DNS — names do not resolve # Must use IP address insteaddocker 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 anymoreMilestone 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.
# Create a user-defined bridge networkdocker network create payment-network # Run containers on the user-defined networkdocker 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 — WORKSdocker 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 namedocker exec payment-api curl http://postgres:5432 # DNS is provided by Docker's embedded DNS server at 127.0.0.11docker exec payment-api cat /etc/resolv.conf# nameserver 127.0.0.11# options ndots:0 # List all networksdocker 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 networkdocker network inspect payment-network# Shows: subnet, gateway, connected containers and their IPs # Connect an existing container to an additional networkdocker 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 networkdocker network disconnect payment-network old-containerMilestone 3: Port Publishing — Making Containers Accessible
# Map host port 8080 to container port 80docker 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 portsdocker run -d \ -p 80:80 \ -p 443:443 \ nginx # Check which port a container publisheddocker inspect payment-api \ --format '{{json .NetworkSettings.Ports}}'# {"8080/tcp":[{"HostIp":"0.0.0.0","HostPort":"8080"}]}Behind the scenes, Docker creates iptables DNAT rules:
# See the iptables rules Docker created for port publishingsudo 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:80Milestone 4: Host Network Mode
# Run with host networking — no isolation, no NAT, no port mappingdocker 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 directlyss -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 hostMilestone 5: Network Troubleshooting
# Inspect a container's network configdocker inspect container-name \ --format '{{json .NetworkSettings.Networks}}' | jq # Check if two containers are on the same networkdocker 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 networkdocker network connect payment-network db # Test connectivity from inside a containerdocker 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 networksudo iptables -L DOCKER-ISOLATION-STAGE-1sudo iptables -L FORWARD# Docker adds isolation rules — verify they are not blocking the trafficCommon 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 |
PLACEMENT PRO TIP**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 THIS**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 / WARNING**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.
{ "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
+------------------------------------------+| 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
# Create a named volumedocker volume create postgres-data # Inspect the volumedocker 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 volumedocker run -d \ --name postgres \ -e POSTGRES_PASSWORD=secret \ -v postgres-data:/var/lib/postgresql/data \ postgres:15 # Create a database tabledocker exec -it postgres psql -U postgres -c \ "CREATE TABLE payments (id SERIAL, amount NUMERIC);" # Remove the containerdocker rm -f postgres # Volume still exists with your datadocker volume ls# DRIVER VOLUME NAME# local postgres-data # Start a new container with the same volumedocker run -d \ --name postgres-new \ -e POSTGRES_PASSWORD=secret \ -v postgres-data:/var/lib/postgresql/data \ postgres:15 # Data is still theredocker exec -it postgres-new psql -U postgres -c \ "SELECT * FROM payments;"# Data persisted through container removal and recreation # List all volumesdocker 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 recoveredMilestone 2: Bind Mounts
# Mount a host directory into a container# Great for development — edit code on host, runs in containerdocker 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 collectiondocker 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 containerdocker inspect app --format '{{json .Mounts}}' | jq# [{# "Type": "bind",# "Source": "/var/log/myapp",# "Destination": "/app/logs",# "Mode": "rw",# "RW": true# }]Milestone 3: tmpfs Mounts
# 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 limitsdocker 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 persistenceMilestone 4: Volumes in Docker Compose
# docker-compose.ymlversion: "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 levelvolumes: postgres-data: # Docker creates and manages this redis-data:# Start with composedocker compose up -d # Volumes are created automatically if declared in the volumes sectiondocker volume ls# local projectname_postgres-data# local projectname_redis-data # Data persists through docker compose downdocker 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 volumesMilestone 5: Backup and Restore Volumes
# Backup a named volume to a tar filedocker 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 backupdocker 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 workeddocker run --rm \ -v postgres-data-restored:/data \ postgres:15 \ ls /data# Should show PostgreSQL data filesWhen 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 |
PLACEMENT PRO TIP**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 THIS**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 / WARNING**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.