Diving into how containers communicate and stay isolated: the five Docker network drivers (bridge, host, none, macvlan/ipvlan, overlay), why a custom bridge with automatic DNS is mandatory in production, how docker0 works, port publishing via iptables, and the networking traps that frequently appear.

After wrapping up data matters in episode 8 — bind mounts, named volumes, tmpfs, backup-restore with tar, and a PostgreSQL that survives recreation — in this episode we cover the last layer before your applications truly "talk": networking. So far we've always connected containers to the outside world with -p 3000:3000 — port forwarding — without ever asking what actually happens behind the scenes. That question becomes urgent the moment you run more than one container: a web app needs to talk to a database, an API needs to talk to Redis. How does that work?
This isn't a trivial question. In production, the network architecture determines security (which containers can "see" other containers), reliability (does the application keep working when a container's IP changes), and scale (how containers on many hosts communicate). Many production incidents have their roots here: an application that works on localhost but not in a container, a database that "can't be reached" because of the wrong network, or two services clashing on ports. Understanding Docker's networking model — not just memorizing commands — will save you hours of debugging.
In this episode we'll dissect the five Docker network drivers, compare the default bridge with a custom bridge (and why automatic DNS changes everything), take apart how docker0 and port publishing work through iptables, and close with a real practice: an application + Redis communicating by container name.
Every container gets its own network namespace — this is the form of network isolation we mentioned in episode 1 (namespaces in the Linux kernel). Inside its namespace, a container has its own interfaces, IP, routing table, and firewall rules that other containers can't see directly. Imagine each container as a house in a neighborhood: every house has its own address and door, and to talk to each other they must connect to the same street — that's the role of the network driver.
When you run docker run without a network flag, Docker automatically attaches the container to the built-in bridge network. Docker also creates several other familiar built-in networks:
docker network lsNETWORK ID NAME DRIVER SCOPE
a1b2c3d4e5f6 bridge bridge local
f6e5d4c3b2a1 host host local
c3d4e5f6a1b2 none null localThose three names (bridge, host, none) are built-in networks that can't be removed. Alongside them, you can create your own networks — and that's where real control lies.
Docker provides five network drivers, each with a different isolation model:
1. Bridge (default) — the default driver for single-host. Docker creates a virtual switch (bridge) named docker0 on the host. Every container on this network gets a private IP (e.g. 172.17.0.x) and an eth0 interface connected to docker0. Containers on the same bridge network can talk to each other by IP; to reach the internet they go through NAT (masquerade) — we'll dissect the mechanism shortly.
2. Host — removes network isolation. The container uses the host's network namespace directly: the host's IP and ports are used as-is, with no NAT and no -p. If the container runs a server on port 8080, it listens on the host's localhost:8080. The advantages: zero network overhead and the lowest latency. The risk: no port isolation — two host-mode containers can't both use port 8080, and all host processes are "visible" from inside the container.
3. None — a container with no network at all. No interface besides loopback. Suitable for workloads that don't need networking and want maximum isolation — e.g. batch-processing jobs that only read volumes and write results, or workers that communicate through files.
4. Macvlan / IPvlan — gives containers a physical identity on the local network. Macvlan gives each container its own MAC address on the LAN subnet, making the container look like a physical device on that network (able to talk directly to the router and other devices). Ipvlan uses IP addresses on the host interface. The downside: it needs correct subnet configuration, and there are connectivity limits with the host (see pitfalls).
5. Overlay — the driver for multi-host, used by Docker Swarm. Containers on many nodes can talk to each other as if on a single virtual network, with traffic encapsulated between nodes. This is the foundation of the cluster we'll study in episodes 16-17.
| Driver | Isolation | Scope | When to Use |
|---|---|---|---|
bridge | Per-network, NAT out | Single host | Default, inter-container communication on one host |
host | None | Single host | Maximum performance, workloads needing host ports directly |
none | Total | Single host | Workloads without networking |
macvlan/ipvlan | Per-physical MAC/IP | Local network | Containers that must appear as devices on the LAN |
overlay | Encapsulated between nodes | Multi host | Swarm clusters |
Now we arrive at the most important decision in this episode. Docker has two kinds of bridge: the default bridge (the built-in bridge network) and the user-defined bridge (a network you create with docker network create). Functionally they're similar — containers on either can talk to each other — but there's one difference that decides production: DNS.
On a user-defined bridge, Docker injects an embedded DNS resolver into every container. As a result, containers can call each other by container name, and that name is resolved automatically to the container's IP — even if the IP changes after a container restart.
On the default bridge, this internal DNS is not available. Containers can only talk to each other by IP. And since container IPs change on every recreate, an application that stores a database IP breaks the moment the container is recreated. That's the emphatic reason: never use the default bridge in production.
Let's prove it with a small experiment. Create a custom network, run two containers, and try calling by name:
docker network create my-net
docker run -d --name redis-cache --network my-net redis:7-alpine
docker run -d --name app --network my-net -e REDIS_URL=redis://redis-cache:6379 my-app:1.0
docker exec app getent hosts redis-cache172.18.0.2 redis-cacheThe line 172.18.0.2 redis-cache proves that the container name redis-cache is resolved to its IP by the embedded DNS. Now imagine using the default bridge: there's no such mechanism — you'd have to hardcode an IP that can change, or use the old --link mechanism (deprecated). This is why, for multi-container setups, user-defined bridges are the standard.
Here are the most commonly used network management commands:
docker network create my-net
docker network ls
docker network inspect my-net
docker network connect my-net redis-cache
docker network disconnect my-net redis-cache
docker run -d --network my-net --name app my-app:1.0docker network create my-net creates a new network with the bridge driver (default). Useful options: --driver, --subnet, --ip-range, --internal (a network without internet access — good for databases that should only be reachable by other containers).docker network inspect my-net shows JSON details: subnet, gateway, and — most useful — the list of containers with their IPs.docker network connect and docker network disconnect attach/detach an already-running container to/from a network, without recreating the container. Useful when an application suddenly needs access to a new network.docker run --network my-net places a container directly on a network when first created.A container can connect to more than one network at once (repeated docker network connect calls). This is a common pattern: a web app connects to the frontend network (for a reverse proxy) and the backend network (for a database), while the database only exists on backend — a zone isolation similar to a DMZ in classic firewalls.
Now let's open the hood. When Docker first starts, it creates the docker0 interface on the host:
ip addr show docker04: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500
link/ether 02:42:ab:1c:2d:3e
inet 172.17.0.1/16 scope global docker0docker0 is a Linux bridge (virtual switch) with IP 172.17.0.1/16. Every container on the default bridge gets an IP from the 172.17.0.0/16 range, and each container's eth0 interface is one of this bridge's "ports" — just like ethernet cables going into a physical switch. When a container on a bridge network calls out (e.g. apt-get update), its packets are masqueraded (NATed) by iptables, so they appear to come from the host IP. Containers can go out to the internet, but the outside world can't come in — unless we open the door.
That door is opened with docker run -p 8080:80. What actually happens? Docker writes a DNAT rule in iptables that translates incoming packets to host IP:8080 into container IP:80:
Chain DOCKER (2 references)
target prot opt source destination
DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.17.0.2:80Because -p 8080:80 maps a host port, only one container can use a given host port — you can't run two containers with -p 8080:80 at the same time. This is one reason why in production you use a single entry point (a reverse proxy, episode 20) that forwards to many containers, rather than publishing every container's port directly.
It's also worth mentioning: Docker's embedded DNS runs at 127.0.0.11 inside every container. This resolver handles container names, network aliases, and forwards unknown queries to the host DNS. That's why inside a container, both getent hosts redis-cache and getent hosts google.com work — both go through the same resolver.
Let's assemble a scenario you'll meet constantly in production: a Node.js application reading from Redis. With a custom bridge, the application just mentions redis-cache as the host — without caring about the IP.
docker network create my-net
docker run -d --name redis-cache --network my-net redis:7-alpine
docker run -d --name app \
--network my-net \
-e REDIS_URL=redis://redis-cache:6379 \
-p 3000:3000 \
my-app:1.0
docker exec app node -e "const r=require('redis').createClient({url:process.env.REDIS_URL}); ..."Inside the application code, the Redis connection is written as:
const client = createClient({ url: "redis://redis-cache:6379" })
await client.connect()
await client.set("kunci", "nilai")Note: redis-cache here isn't an internet address — it's a container name resolved by the embedded DNS at 127.0.0.11 inside the app container to Redis's IP. This is the magic of the custom bridge: application configuration no longer depends on IPs, so when Redis is recreated (IP changes), the application keeps working without changing anything.
Default bridge without DNS. Containers on the default bridge can only talk by IP, and the IP changes on every recreate. Use a user-defined bridge for all inter-container communication.
Port clashes on the host driver. With --network host, port isolation is completely gone: the container uses host ports directly, so two host-mode containers can't share the same port, and you lose the flexibility of -p. The host driver is only appropriate for specific workloads that genuinely need maximum performance.
macvlan connectivity limits. A macvlan container cannot communicate with the host on which it runs (by design, to prevent loops on the bridge). An application that needs to talk to the host (e.g. a service on localhost) will fail with macvlan. If that need exists, consider another driver or additional interface configuration.
A new container failing to reach the internet on --network host or a none network: this isn't a bug — it's exactly the isolation you asked for. An internal network even deliberately blocks outbound access; make sure containers that need the internet are on the right network.
Storing IPs between containers. Never hardcode a container's IP in application configuration. With a custom bridge, just use the container name — the IP is a detail that may change at any time.
Tip
To debug networking problems, three main weapons: docker network inspect my-net (see IPs and network members), docker exec app getent hosts <nama> (test name resolution), and docker exec app ping <ip> or nc -zv <host> <port> (test connectivity). On a minimal Alpine image, ping and nc may not be installed — install them via apk add iputils netcat-openbsd in a temporary container for debugging.
In this episode 9 you've dived into container networking down to the kernel level: understanding that network isolation is based on network namespaces, learning the five drivers (bridge for single-host, host which removes isolation, none for total isolation, macvlan/ipvlan for physical identity on the LAN, and overlay for multi-host Swarm), understanding why the default bridge can't be used in production (no automatic DNS), creating and managing your own networks with docker network create/ls/inspect/connect/disconnect, taking apart how docker0 works as a virtual switch, learning that -p actually writes iptables DNAT rules, and proving that an application + Redis can talk by container name thanks to the embedded DNS at 127.0.0.11.
Core takeaway: inter-container communication must go through a user-defined bridge and container names — not IPs. Networking is an architectural layer: by designing the right networks (which containers may "see" which), you also design your application's security.
Looking at all those long docker run command chains — volumes, networks, env, ports — you might be thinking: "there must be a cleaner way to manage all this?" That's exactly the next topic. In the next episode, episode 10, we'll learn Docker Compose: declaring an entire multi-container application — web, API, database, Redis, networks, volumes — in a single compose.yaml file, and replacing those sequences of docker run commands with one docker compose up -d. See you in episode 10!