Docker Networking and Volumes

Networking: the four modes

Every container gets its own network namespace by default (see Docker Fundamentals) — what differs between the modes below is what that namespace is connected to.

Bridge (the default) — Docker creates a virtual bridge interface on the host (docker0 for the default network, or a custom one for user-defined networks). Each container gets a virtual ethernet pair: one end inside the container's namespace, one end attached to the bridge. The bridge acts like a virtual switch, and the host's kernel NATs traffic between the bridge's private subnet and the outside world.

graph TD subgraph outside["Outside world"] client["client request to host-ip:8080"] end subgraph host["Host machine"] iptables["iptables NAT rule<br/>host:8080 to bridge:80"] subgraph bridge["docker0 bridge (172.17.0.0/16)"] vethA["veth (host side)"] vethB["veth (host side)"] end contA["Container A<br/>172.17.0.2:80"] contB["Container B<br/>172.17.0.3:80"] end client --> iptables --> vethA --> contA vethB --> contB contA -.->|"can reach via bridge subnet"| contB

This is what actually happens when a request reaches http://<host-ip>:8080 and gets routed to container A's port 80: the kernel's NAT table (set up by docker run -p 8080:80) rewrites the destination and forwards it onto the bridge, which delivers it to container A's veth pair, which delivers it inside the container's network namespace as if it arrived directly on port 80.

Host — the container shares the host's network namespace entirely. No bridge, no NAT, no port mapping — a process binding to port 80 inside the container is binding to port 80 on the host, directly. Fastest option (no NAT overhead), but the container loses all network isolation: it can bind any port the host allows, and it sees the host's actual network interfaces.

None — no networking at all beyond a loopback interface. Used for batch jobs that do no network I/O, or when a custom, unusual networking setup is being wired up manually.

Custom bridge networks (the practical default for multi-container apps)docker network create my-app-net creates a new bridge with its own subnet, and containers attached to it get something the default bridge doesn't provide: automatic DNS resolution by container name.

Why the default bridge isn't enough for multi-container apps

docker run -d --name db postgres:16
docker run -d --name api my-api-image

Both containers land on the default bridge network. Inside api, trying to connect to db by hostname (postgres://db:5432/...) fails to resolve — the default bridge network provides no built-in DNS between containers, only raw IP connectivity, and even that requires knowing the other container's IP in advance (which isn't stable across restarts).

Fix: create a custom network and attach both containers to it.

docker network create my-app-net
docker run -d --name db --network my-app-net postgres:16
docker run -d --name api --network my-app-net my-api-image

Now, inside api, postgres://db:5432/... resolves correctly — Docker runs an embedded DNS server for every custom network, mapping each attached container's name to its current IP automatically, including across container restarts (a restarted container may get a new IP; the DNS entry follows it). This is the mechanism docker-compose relies on to let services address each other by name (covered in a follow-up page on Docker Compose).

Volumes: three ways to persist data, and when each applies

A container's writable layer is deleted with the container (docker rm) — anything meant to outlive the container needs to live somewhere else.

graph LR subgraph container["Container filesystem view"] mnt["/data (mount point)"] end subgraph opt1["Named volume"] v["/var/lib/docker/volumes/mydata/_data<br/>(managed by Docker)"] end subgraph opt2["Bind mount"] b["/home/user/project/data<br/>(arbitrary host path)"] end subgraph opt3["tmpfs"] t["RAM only<br/>never written to disk"] end mnt -.->|"-v mydata:/data"| v mnt -.->|"-v /home/user/project/data:/data"| b mnt -.->|"--tmpfs /data"| t
  • Named volume (docker volume create mydata, then docker run -v mydata:/data ...) — Docker manages where on the host this actually lives (/var/lib/docker/volumes/... by default). The right default for anything a database needs to persist: Docker owns the lifecycle, it survives docker rm, and it's portable across different volume drivers (local disk, NFS, cloud block storage) without the container's own configuration changing.
  • Bind mount (docker run -v /home/user/project:/app ...) — maps an exact, specific host path into the container. The right tool for local development: edit source code on the host, see the change immediately inside a running container without rebuilding the image. The wrong tool for production data, because it hardcodes a host filesystem path into the deployment, which doesn't generalize across different hosts.
  • tmpfs mount (docker run --tmpfs /app/cache ...) — memory-backed, never touches disk, gone the moment the container stops. Right for short-lived secrets or scratch space that should never be persisted or swapped to disk at all.

Common pitfall

Publishing a port (-p 8080:80) is not the same as attaching to a network, and mixing up the two mental models causes confusion: -p exposes a container's port to the outside world through the host; container-to-container communication on a shared custom network needs neither -p nor knowledge of any host port at all — containers reach each other directly through the bridge, by container name, using the container's internal port. A container's own service should be reachable from a sibling container even with zero -p flags, as long as both are on the same custom network.