Docker from First PrinciplesPost 8 of 18 · 18 of 18 written

8. Networking

Docker · 8 of 183 min readOct 2026

Each container gets its own network namespace: its own interfaces, IP address, routing table and localhost. Docker connects those namespaces to each other and to the outside world with virtual bridges, NAT rules and a built-in DNS server.

How the default bridge works

On Linux, Docker creates a virtual switch called docker0, usually on the 172.17.0.0/16 subnet. For each container it creates a veth pair, a virtual cable with two ends: one end becomes eth0 inside the container, the other plugs into the bridge. Outgoing traffic is NAT-ed (masqueraded) to the host’s address, which is how containers reach the internet.

Incoming traffic needs port publishing. -p 8000:8000 adds a destination-NAT rule: packets arriving at the host’s port 8000 are rewritten to the container’s IP and port 8000. Without -p, nothing outside the host can reach the container. docker port <name> shows the active mappings.

User-defined networks and DNS

docker network create ai-net
docker run -d --name db --network ai-net pgvector/pgvector:pg16
docker run -d --name api --network ai-net -e DATABASE_HOST=db myapi

Containers on the same user-defined network resolve each other by name through Docker’s embedded DNS server (at 127.0.0.11 inside each container). The legacy default bridge has no name resolution, which is why you should always create a network, or let Compose create one. Containers on different networks can’t talk to each other at all, which is a cheap way to segment a stack.

Network drivers

Driver What it does When to use it
bridge Private network on one host with NAT The default for almost everything
host No network namespace; the container uses the host’s stack directly Maximum network performance, multi-node GPU training
none Only a loopback interface Sandboxing untrusted or LLM-generated code
overlay Network spanning several Docker hosts Docker Swarm; rare outside it
macvlan, ipvlan Container gets an address on the physical LAN Special cases, appliances

The three classic confusions

  1. The app binds to 127.0.0.1 inside the container. Port publishing forwards to the container’s eth0, which a loopback-only server never listens on. Bind to 0.0.0.0: uvicorn --host 0.0.0.0, jupyter lab --ip 0.0.0.0.
  2. One container uses localhost to reach another. localhost is the container itself. Use the other container’s name on a shared network: postgresql://db:5432, http://qdrant:6333.
  3. A container needs a service running on the host. Use host.docker.internal. Docker Desktop provides it automatically; on Linux add --add-host=host.docker.internal:host-gateway. On your Mac, this is how a containerised app reaches Ollama running natively with Metal acceleration: http://host.docker.internal:11434.

Exposure and firewalls

-p 8888:8888 listens on every interface of the host, not just localhost. On Linux, Docker writes its own firewall rules, which take effect before tools like ufw, so a published port can be reachable from the internet even when ufw says it’s blocked. Jupyter, Ollama, Redis and vector databases left open this way are a common real-world compromise. Publish internal services on 127.0.0.1 (-p 127.0.0.1:8888:8888) and reach them through an SSH tunnel or an authenticated reverse proxy.

Multi-node training

Distributed training across machines uses NCCL over InfiniBand or RoCE, which needs direct access to the network hardware. Such jobs typically run with --network host, the InfiniBand devices passed in (--device /dev/infiniband), the IPC_LOCK capability and an unlimited memlock limit. In practice a scheduler (Slurm, Kubernetes) sets this up for you; Part 15 covers those.