Guide: Docker networks for self-hosting — bridge, host, macvlan, ipvlan

«Containers see each other by name? why isn’t the port forwarded? why macvlan?» — networks in Docker trip up almost every self-hosted user. Let’s review the built-in network drivers according to the official documentation and get to the point: what to choose for reverse proxy, for Pi-hole, for the monitoring stack.

Built-in drivers

Docker Engine on Linux provides several network drivers:

  • bridge — the default driver for single containers on one host. Containers in the same bridge network can talk to each other, isolated from others.
  • host — removes network isolation: the container uses the host’s network stack directly. Fast, but the container can “see” all host ports; convenient for things that need to see the entire network (for example, device discovery).
  • none — complete isolation: the container has no network at all.
  • overlay — connects multiple Docker daemons (Swarm) into a single network for cross-host communication.
  • macvlan — the container gets its own MAC and appears as a separate device on your physical network (its own IP from the LAN subnet).
  • ipvlan — similar to macvlan, but without separate MACs; convenient when the provider/switch limits the number of MACs, and for VLAN work.

The main fork: default bridge vs user-defined bridge

This is what trips people up most often.

  • In the default bridge network (where a container lands if you don’t specify a network) containers cannot reach each other by name — only by IP. And IPs are assigned dynamically, which is a pain.
  • In the user-defined bridge network (created by you) the built-in Docker DNS works (address 127.0.0.11), and containers resolve each other by container name. This is almost always what you need.

Practical takeaway: always create your own network rather than rely on the default:

docker network create appnet
docker run -d --name db --network appnet postgres:16
docker run -d --name app --network appnet myapp   # reach db as host "db"

In docker-compose this happens automatically: Compose creates a separate network for the project, and services see each other by service names. So in compose you write db:5432 in the app config, not an IP.

Port forwarding

Container ports in a bridge network are accessible from the host itself, but to be visible from outside the host, you must publish the port with the -p/--publish flag:

docker run -d -p 8080:80 nginx      # host:container

Security caveat: -p 8080:80 listens on all interfaces. If the service should be available only locally (for example behind a reverse proxy), bind to the loopback: -p 127.0.0.1:8080:80.

What to choose for common tasks

  • Reverse proxy (Nginx Proxy Manager, Caddy, Traefik) + services: one user-defined bridge network, publish only 80/443 to the outside via the proxy, keep the rest of the services without -p — the proxy talks to them by name.
  • Pi-hole/AdGuard, needing a true LAN IP: macvlan — the container gets an address in your subnet and appears on the network as a separate device.
  • Monitoring/service that needs access to all host ports: host network (aware of the loss of isolation).
  • Shared network namespace for two containers: --network container:<name> — the second container shares the first’s interface and communicates through 127.0.0.1 (a common pattern for attaching a sidecar/VPN container).

A sober assessment

  • macvlan — not a free lunch. By default the host cannot talk to its macvlan containers (isolation of the parent interface); you need a separate macvlan gateway trick. Plus not all Wi‑Fi adapters and cloud providers cope with this.
  • host network breaks isolation. Saves on NAT, but the container gains access to the host’s entire network stack — use only where you consciously need it.
  • Default network — source of half the “why doesn’t it resolve.” If something can’t see its neighbor by name, you’ve almost always forgotten to create your own network.

What to do

  1. Make user-defined bridge your rule for any multi-container stack (or simply use docker-compose, where this is built in).
  2. Publish externally at a minimum — ideally only 80/443 for the reverse proxy, everything else kept inside the network and bound to 127.0.0.1 if publication is still needed.
  3. macvlan — only when you truly need a LAN IP (Pi-hole/AdGuard, smart-home devices), and decide in advance the question of “host↔container.”
  4. Check connectivity from within a container: docker exec -it app ping db and getent hosts db — you’ll immediately see whether the built-in DNS works.

Sources

How are your Docker stack networks organized — one shared network, project-level segmentation, macvlan for individual services? Have you run into “can’t resolve by name” on the default network?