Docker Networking Fundamentals¶
"Why can't these two containers talk to each other" and "why can't I reach this container from the host" are two of the most common container questions, and both come down to understanding Docker's network drivers.
How it works¶
Network drivers¶
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
abc123def456 bridge bridge local
def456abc789 host host local
789abc123def none null local
Every Docker install ships with three built-in networks:
bridge(default) — containers get their own private IP on a virtual bridge interface (docker0on the host) and reach the outside world via NAT. On the default bridge, containers can reach each other by IP address but not by name; a user-defined bridge network adds automatic DNS resolution by container name, which is why running everything on the default network and juggling IPs by hand is considered bad practice.host— the container shares the host's network namespace directly, no isolation, no port mapping needed (and none possible — the container binds host ports directly). Faster, but zero network isolation from the host.none— no external networking: the container gets only a loopback interface.
User-defined bridge networks (the practical default for multi-container apps)¶
docker network create app_net
docker run -d --name db --network app_net postgres:16
docker run -d --name api --network app_net -e DB_HOST=db myapp
On a user-defined network, Docker runs an embedded DNS server that resolves container names to their IPs automatically — the api container can reach the database at the hostname db, with no manual IP configuration or /etc/hosts editing. This is the single biggest practical reason to always create an explicit network rather than relying on the default bridge.
Port publishing: -p explained¶
-p <host_port>:<container_port> — this is NAT, not a direct bind: traffic hitting host_ip:8080 gets forwarded to port 80 inside the container. Critically, Docker manipulates the host's iptables/nftables rules directly to implement this — a ufw/firewalld rule that appears to block the port may have no effect, because Docker's own rules are evaluated first in a different chain. This is a very common source of confusion: an operator sets up ufw deny 8080, confirms with ufw status that it's denied, and the port is still reachable anyway, because ufw's rules don't automatically cover Docker's own DOCKER/DOCKER-USER chains. To publish only to the host itself, bind to loopback: docker run -d -p 127.0.0.1:8080:80 nginx.
Inspecting a container's actual network config¶
This shows the container's actual IP address(es), gateway, and which network(s) it's attached to — useful when docker network ls confirms the network exists but you need to confirm a specific container is actually on it.
Container-to-container traffic without exposing a port to the host at all¶
Two containers on the same user-defined network can reach each other on any port the listening one exposes, with no -p flag needed at all — -p is only for exposing a port to the host machine / outside world. A database container that only the API container needs to reach should have no -p published for its database port — publishing it anyway exposes it to anything that can reach the host, not just the API container.
Ubuntu/Debian vs. RHEL-family differences¶
Docker's networking model is identical across distributions (it's implemented by the Docker daemon itself, not distribution-specific). The practical difference is firewall interaction: on a RHEL-family host running firewalld, Docker's manipulation of the underlying nftables/iptables rules can similarly bypass firewalld's own zone rules — the same "my firewall rule doesn't seem to apply to this container port" surprise applies there too.
Practical exercise¶
- Create a user-defined network and run two containers on it (e.g.,
nginxand a simplealpinecontainer withsleep infinity). - From inside the alpine container (
docker exec -it <name> sh), runwget -qO- http://nginx(using the container name, not an IP) and confirm it resolves and responds. - Run a third container not attached to that network and confirm the same
wgetby name fails — proving network membership, not just "Docker is running," is what grants reachability.
How to verify you did it correctly¶
- Step 2:
wgetshould succeed and print nginx's default HTML page, with no IP address used anywhere in the command. - Step 3: the same command from an unattached container should fail with a DNS resolution error (the name
nginxis meaningless outside that specific user-defined network's embedded DNS).
Common mistakes¶
- Assuming a
ufw/firewallddeny rule blocks a Docker-published port. It frequently doesn't — Docker's own iptables rules (theDOCKER/DOCKER-USERchains) are evaluated independently. If you need the host firewall to actually govern Docker-published ports, rules need to go into theDOCKER-USERchain specifically, not the genericINPUTchainufw/firewalldmanage. - Publishing a port with
-pfor purely inter-container communication that didn't need to be exposed to the host/outside world at all. - Relying on the default
bridgenetwork and hardcoding container IPs, which change on recreation — a user-defined network's name-based DNS avoids this entirely.
⚠ Dangerous commands¶
docker network pruneremoves all networks not currently used by at least one container — if a network is defined but its containers are temporarily stopped (not removed), the network itself can still be pruned, requiring you to recreate it before starting those containers again.- Publishing a database or admin-panel port directly with
-p <port>:<port>without a host firewall rule restricting source IPs exposes it to the entire network the host is reachable from — treat any-pthe same way you'd treat opening a firewall port, because that's effectively what it does.
Related articles¶
- Understanding systemd
- Linux Firewall with nftables and iptables
- Nginx Reverse Proxy and 502/504 Troubleshooting
Review questions¶
- Why does the default
bridgenetwork not provide name-based resolution between containers, while a user-defined bridge network does? - Why might a
ufw deny <port>rule appear to have no effect on a port published by Docker? - What's the practical difference between the
bridgeandhostnetwork drivers in terms of isolation? - When would you not want to publish a container's port with
-p, even though the port is genuinely in use?