Container networking is built on Linux network namespaces — each container gets its own isolated network stack with its own interfaces, routing table, and firewall rules. The container engine (Docker or Podman) creates virtual network infrastructure on the host to connect containers to each other and to the outside world.
Understanding container networking is essential for troubleshooting connectivity problems, securing multi-container applications, and designing systems where containers need to communicate. The concepts apply equally to Docker and Podman.
Software bridge on the host. Containers get private IPs. NAT used for outbound. Port mapping for inbound.
Use: Single-host multi-container apps.
Container shares the host's network namespace. No isolation — uses host IP directly. Best performance.
Use: When performance matters and isolation isn't needed.
No network interface except loopback. Complete network isolation.
Use: Batch jobs, security-sensitive workloads.
Multi-host networking. Spans multiple Docker hosts. Requires Swarm or Kubernetes.
Use: Distributed multi-host deployments.
Container gets its own MAC address and appears as a physical device on the network.
Use: Legacy apps needing a real network presence.
Similar to macvlan but shares host MAC. Better for environments that restrict MAC spoofing.
Use: Cloud environments, strict switch configs.
# List all networks docker network ls # Inspect the default bridge network docker network inspect bridge # Create a custom bridge network docker network create mynet docker network create --driver bridge mynet # Create with specific subnet and gateway docker network create \ --driver bridge \ --subnet 172.20.0.0/16 \ --gateway 172.20.0.1 \ --ip-range 172.20.1.0/24 \ appnet # Run container on a specific network docker run -d --name web --network appnet nginx:latest docker run -d --name db --network appnet mysql:8.0 # Connect a running container to a network docker network connect appnet mycontainer # Disconnect container from network docker network disconnect appnet mycontainer # Remove a network (all containers must be disconnected) docker network rm mynet docker network prune # remove all unused networksdocker network ls output:
On custom bridge networks, Docker provides automatic DNS resolution by container name — the killer feature that makes multi-container apps work cleanly:
# Create a network docker network create appnet # Start a database container docker run -d \ --name postgres \ --network appnet \ -e POSTGRES_PASSWORD=secret \ postgres:15 # Start app container on same network docker run -d \ --name webapp \ --network appnet \ -e DATABASE_URL=postgresql://postgres:secret@postgres:5432/mydb \ myapp:latest # The app connects to 'postgres' by NAME — not by IP # Docker DNS resolves 'postgres' to the container's IP automatically # Verify DNS works from inside a container docker exec webapp ping postgres docker exec webapp nslookup postgres docker exec webapp curl http://nginx/health
bridge network does NOT have automatic DNS.
Containers on the default bridge can only reach each other by IP address.
On a custom bridge network, containers find each other by name.
This is why every Docker Compose stack creates its own network automatically.
docker run -d --name mydb --network appnet \
--network-alias database \
--network-alias db \
postgres:15
Now mydb, database, and db all
resolve to the same container.
# Map host port 8080 to container port 80 docker run -d -p 8080:80 nginx:latest # Map on a specific host interface only (more secure) docker run -d -p 127.0.0.1:8080:80 nginx:latest docker run -d -p 192.168.1.100:8080:80 nginx:latest # Map multiple ports docker run -d \ -p 80:80 \ -p 443:443 \ nginx:latest # Publish ALL exposed ports to random host ports docker run -d -P nginx:latest # Check what ports are mapped docker port mycontainer docker ps # PORTS column shows mappings # Map UDP port docker run -d -p 5353:5353/udp dns:latestdocker port output:
-p 8080:80 binds to all interfaces (0.0.0.0)
— the port is reachable from anywhere. -p 127.0.0.1:8080:80
binds only to localhost — only processes on the same host can reach it.
Use this for services that should only be accessed via a reverse proxy.
The container shares the host's network namespace — no NAT, no port mapping needed, maximum performance:
# Run with host networking docker run -d --network host nginx:latest # The container now listens on the host's port 80 directly # No -p flag needed or allowed with --network host curl http://localhost:80 # Podman equivalent podman run -d --network host nginx:latest # Verify — container sees host interfaces docker run --rm --network host alpine ip addr
Podman uses the same networking concepts as Docker but with slightly different commands and the netavark/CNI backend:
# List networks podman network ls # Create a network podman network create appnet podman network create --subnet 172.20.0.0/16 appnet # Inspect a network podman network inspect appnet # Run containers on the network podman run -d --name db --network appnet postgres:15 podman run -d --name web --network appnet nginx:latest # Connect/disconnect podman network connect appnet mycontainer podman network disconnect appnet mycontainer # Remove network podman network rm appnet podman network prune # Rootless Podman — check the slirp4netns or pasta backend podman info | grep -A5 "network" # DNS works same as Docker on custom networks podman exec webapp ping db
slirp4netns or pasta for networking
instead of kernel bridge interfaces. This means rootless containers
cannot bind to privileged ports (below 1024) and have slightly
different performance characteristics than root containers.
Compose automatically creates a network for each stack — all services in the stack can reach each other by service name:
# docker-compose.yml — explicit network configuration cat > docker-compose.yml << 'EOF' services: nginx: image: nginx:alpine networks: - frontend ports: - "80:80" app: image: myapp:latest networks: - frontend - backend db: image: postgres:15 networks: - backend networks: frontend: driver: bridge backend: driver: bridge internal: true # no external access EOF # internal: true means the backend network has no internet access # app can reach both nginx (frontend) and db (backend) # db can only be reached by app — not by nginx or external traffic docker compose up -d # View networks created by compose docker network ls | grep myproject
internal: true network means it has no outbound
internet access and is unreachable from the host network — only
containers explicitly on that network can talk to it. This is
proper defense-in-depth for containerized applications.
# Check what networks a container is on docker inspect mycontainer | jq '.[0].NetworkSettings.Networks' # Get a container's IP address docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mycontainer # Test connectivity from inside a container docker exec mycontainer ping google.com docker exec mycontainer curl -v http://db:5432 docker exec mycontainer nslookup db # Run a diagnostic container on the same network docker run --rm -it --network appnet nicolaka/netshoot # netshoot has ping, nslookup, curl, tcpdump, nmap, ss, etc. # Watch container traffic with tcpdump on the host # Find the veth interface for your container docker inspect mycontainer | grep -i "sandboxkey\|veth" sudo tcpdump -i veth123abc -n # Check iptables rules Docker created sudo iptables -t nat -L -n --line-numbers | grep -A5 DOCKER sudo iptables -L DOCKER -n # Check host routing table ip route show ip route show table all | grep docker
macvlan gives containers their own MAC address and makes them appear as physical devices on your LAN — useful for legacy apps that need a real network presence:
# Create a macvlan network docker network create \ --driver macvlan \ --subnet 192.168.1.0/24 \ --gateway 192.168.1.1 \ --ip-range 192.168.1.192/27 \ -o parent=eth0 \ macnet # Run a container with a specific IP on the LAN docker run -d \ --name legacy-app \ --network macnet \ --ip 192.168.1.200 \ mylegacyapp:latest # The container now has 192.168.1.200 on your LAN # Other hosts on the LAN can ping and connect to it directly ping 192.168.1.200 # Enable promiscuous mode on the host interface sudo ip link set eth0 promisc on
| Command | What it does |
|---|---|
| docker network ls | List all networks |
| docker network create NAME | Create a custom bridge network |
| docker network inspect NAME | Detailed network information |
| docker network connect NET CONTAINER | Add container to network |
| docker network disconnect NET CONTAINER | Remove container from network |
| docker network rm NAME | Remove a network |
| docker network prune | Remove all unused networks |
| docker run --network NAME | Run container on specific network |
| docker run --network host | Use host network namespace |
| docker run --network none | No network access |
| docker run -p HOST:CONT | Map host port to container port |
| docker run -p 127.0.0.1:H:C | Map to localhost only |
| docker port CONTAINER | Show port mappings |
| docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' C | Get container IP |
| docker run --rm -it --network NET nicolaka/netshoot | Diagnostic container on network |
| Driver | Isolation | Performance | Best For |
|---|---|---|---|
| bridge | Good | Good | Default — single host multi-container |
| host | None | Best | Performance-critical, trusted images |
| none | Complete | N/A | Batch jobs, maximum security |
| overlay | Good | Good | Multi-host Swarm/Kubernetes |
| macvlan | Good | Best | Legacy apps needing LAN presence |
| ipvlan | Good | Best | Environments restricting MAC spoofing |
| Scenario | DNS Available? | Resolution |
|---|---|---|
| Default bridge network | ❌ No | IP addresses only — use --link (deprecated) |
| Custom bridge network | ✅ Yes | Container name resolves automatically |
| Docker Compose stack | ✅ Yes | Service name resolves automatically |
| Network alias | ✅ Yes | Additional DNS names per container |
| Host network | Host DNS | Uses host /etc/resolv.conf |