Reading time: ~15 minutes Audience: Homelabbers running 5+ Docker containers who are tired of port collisions, localhost confusion, and containers that can’t talk to each other Last tested: June 2026 — Docker 27.x, Docker Compose v2, Traefik 3.2


Introduction: The Docker Networking Trap

You started with one Docker container — maybe Pi-hole for ad blocking. You ran docker run -p 53:53 pihole/pihole and it worked. Then you added Home Assistant (-p 8123:8123), then Jellyfin (-p 8096:8096), then Portainer (-p 9443:9443). Every service got a unique port number, and you memorized them like phone extensions at a small office.

Then the problems started. Your Jellyfin container couldn’t reach your Sonarr container. Your Home Assistant couldn’t discover your MQTT broker. You tried localhost:1883 and got connection refused. You tried the container IP (172.17.0.5:1883), and it worked — until Docker restarted and assigned a new IP. The default bridge network that makes Docker feel effortless during your first docker run becomes the single biggest source of frustration the moment you have more than three containers.

This guide is about one thing: designing Docker networks that don’t rot. You will learn when to use bridge vs host vs macvlan, why user-defined bridges are the backbone of every clean Docker Compose stack, how to integrate Traefik without tangling your network topology, and how to segment containers by service role so a compromised Plex container cannot reach your Vaultwarden database.

By the end, you will have a repeatable network architecture pattern that works whether you run 5 containers on a Raspberry Pi or 50 on a Proxmox node.


Understanding Docker Network Drivers

Docker ships with five network drivers, but only three matter for a homelab. Here is what they do, when to use them, and — critically — when to avoid them.

Driver Scope DNS External Access Best For
bridge (default) Container-to-container on same host ❌ No automatic DNS Via port mapping (-p) Nothing — avoid for multi-container setups
bridge (user-defined) Container-to-container on same host ✅ Built-in DNS resolution Via port mapping The standard — every Docker Compose stack
host Shares host network namespace N/A (host’s DNS) All ports exposed directly High-performance apps (Plex transcoding, game servers)
macvlan Container gets its own MAC/IP on LAN N/A (LAN DHCP/DNS) Direct LAN access Containers needing static LAN IPs (Pi-hole, Home Assistant discovery)
overlay Multi-host swarm Via routing mesh Multi-node Docker Swarm (rare in homelabs)

Default Bridge: The Silent Killer

When you run docker run without explicitly specifying a network, every container lands on the default bridge network. This network has two fatal flaws for any setup with more than three containers:

  1. No DNS resolution. Containers on the default bridge can only reach each other by IP address — and those IPs change every time a container restarts. This is why ping mysql fails while ping 172.17.0.3 works, until Docker restarts and MySQL moves to 172.17.0.6.

  2. No isolation. Every container on the default bridge can talk to every other container. There is no segmentation, no access control, no way to say “Jellyfin can talk to Sonarr but not to Vaultwarden.”

The fix: Never use the default bridge for anything. Create a user-defined bridge network for every group of related services. We will cover this pattern in detail below.

Host Network: Raw Performance, Zero Isolation

The host driver removes Docker’s network namespace entirely. Your container sees the host’s network interfaces directly — if the host listens on 0.0.0.0:32400, the container binds to 0.0.0.0:32400. No port mapping. No NAT overhead. No Docker network proxy process sitting between your application and the network card.

When to use host networking:

  • Plex Media Server — transcoding benefits from direct NIC access and avoids the Docker userland proxy bottleneck.
  • Game servers (Minecraft, Valheim) — lower latency, no port mapping to manage.
  • Network monitoring tools (ntopng, Wireshark in a container) — need raw packet access.
  • Pi-hole with DHCPDHCP requires broadcast traffic Docker bridge filters out.

When to avoid host networking:

  • Every other service. Host networking is the nuclear option. You lose DNS-based service discovery, container isolation, and port conflict detection. Two containers on host cannot both bind to port 80.

Macvlan: When Your Container Needs a LAN Identity

Macvlan gives your container a real MAC address and IP on your physical LAN, indistinguishable from a physical machine. Your router sees it. Your other devices see it. mDNS and broadcast protocols work natively.

Legitimate macvlan use cases:

  • Home Assistant — needs mDNS and DHCP discovery for ESPHome, Apple HomeKit, and Chromecast.
  • Pi-hole as primary DNS — your router expects a static LAN IP for DNS; macvlan gives Pi-hole its own 192.168.1.53.
  • Legacy apps that hardcode IP addresses — if some crusty service only connects to 192.168.1.100, macvlan lets you give a container that exact IP.

The macvlan caveat: A macvlan container cannot communicate with its Docker host. This is by design — macvlan isolates the container at Layer 2. If you need host-to-container communication over macvlan, you must create a sub-interface or a second macvlan on the host. For most homelabbers, the solution is simpler: only use macvlan for the one or two containers that genuinely require it, and put everything else on user-defined bridges.


The Power of User-Defined Bridge Networks

A user-defined bridge is the backbone of every well-architected Docker Compose stack. When you create a new bridge network with docker network create my-net or define a networks: block in Compose, you unlock three capabilities the default bridge cannot provide.

Automatic DNS Resolution

Containers on the same user-defined bridge can reach each other by service name or container name. This is the single feature that makes multi-container Docker viable.

# docker-compose.yml
services:
  app:
    image: my-app
    networks:
      - backend
    environment:
      - DATABASE_URL=postgres://db:5432/mydb  # ← service name, not IP

  db:
    image: postgres:16
    networks:
      - backend

networks:
  backend:
    driver: bridge

The app container connects to db:5432. Docker’s embedded DNS server resolves db to the container’s IP. No hardcoded IPs. No environment variables full of 172.x.x.x. If the db container restarts and gets a new IP, the DNS entry updates automatically. This is how every professional Docker deployment works.

Network-Level Isolation

User-defined bridges give you network segmentation at zero cost. Create one bridge per service group, and containers on different bridges cannot reach each other unless you explicitly connect them to multiple networks.

networks:
  media:       # Jellyfin, Sonarr, Radarr, Prowlarr, qBittorrent
    driver: bridge
  proxy:       # Traefik, Authelia, CrowdSec
    driver: bridge
  storage:     # Nextcloud, MariaDB, Redis
    driver: bridge
  monitoring:  # Grafana, Prometheus, Uptime Kuma
    driver: bridge
  dns:         # Pi-hole, Unbound
    driver: bridge

Why segment by service role? If a vulnerability in Jellyfin allows remote code execution, the attacker lands on the media network. From there, they can reach Sonarr and qBittorrent — annoying but not catastrophic. They cannot reach your Vaultwarden database on the storage network or your Traefik dashboard on proxy. Network segmentation turns a full-compromise scenario into a contained incident.

The trade-off: if a container genuinely needs cross-segment access (e.g., Traefik must reach every service it proxies), you attach it to multiple networks:

services:
  traefik:
    networks:
      - proxy
      - media
      - storage
      - monitoring
      - dns

Traefik is the exception, not the rule. Every other container should live on exactly one network.

The Docker Compose Default Network

When you run docker compose up, Compose automatically creates a user-defined bridge named <project>_default. Every service without an explicit networks: entry joins this network and gets DNS resolution. This is fine for a single-stack homelab with 5-8 containers. The moment you have multiple Compose files or 10+ containers, switch to explicit named networks.

Related: If you are still running containers with raw docker run commands, read our Docker Compose for Beginners guide and migrate to Compose today. You are fighting with one hand tied behind your back.


Integrating Traefik as the Network Gatekeeper

In 2026, Traefik is the standard reverse proxy for homelab Docker environments. It discovers containers via Docker socket labels, routes HTTP/HTTPS traffic, and auto-provisions Let’s Encrypt certificates — all without touching a reverse proxy GUI. But Traefik’s network architecture has specific requirements that trip up newcomers.

The Traefik Network Model

Traefik needs to be on the same Docker network as every container it proxies. This is non-negotiable: Traefik routes requests by connecting to the backend container’s internal IP via the shared Docker bridge. If Traefik is on network-A and your app is on network-B with no overlap, Traefik returns 502 Bad Gateway and nothing in the logs tells you why.

The correct pattern:

# traefik/docker-compose.yml
services:
  traefik:
    image: traefik:v3.2
    networks:
      - proxy        # Traefik's own management network
      - media        # Connects to Jellyfin/Sonarr
      - storage      # Connects to Nextcloud
      - monitoring   # Connects to Grafana/Uptime Kuma
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    # ... rest of Traefik config

networks:
  proxy:
    external: true    # Created separately: docker network create proxy
  media:
    external: true
  storage:
    external: true
  monitoring:
    external: true

And on the application side, each service joins its functional network plus, optionally, the proxy network if you want Traefik to route to it:

# jellyfin/docker-compose.yml
services:
  jellyfin:
    image: jellyfin/jellyfin:latest
    networks:
      - media
      - proxy       # Traefik needs this to route traffic
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.jellyfin.rule=Host(`media.${DOMAIN}`)"
      - "traefik.http.services.jellyfin.loadbalancer.server.port=8096"

networks:
  media:
    external: true
  proxy:
    external: true

The golden rule: Every service Traefik proxies must either share a network with Traefik, or Traefik must be explicitly attached to that service’s network. Pick one pattern and stick with it. Most homelabbers prefer attaching Traefik to every network (the “hub” model) because it keeps application Compose files simple.

Choosing a reverse proxy? Our Traefik vs Nginx Proxy Manager vs Caddy guide breaks down which reverse proxy fits your homelab. For Docker networking specifically, Traefik has the cleanest network model because it natively understands Docker labels and service discovery.

Traefik + External Networks: Avoiding the 502

The most common Traefik networking error: you define a network in your app’s Compose file but forget to declare it as external: true. Docker Compose creates a new network with a project-prefixed name (e.g., jellyfin_media) instead of joining the existing media network. Traefik is on media, Jellyfin is on jellyfin_media, they cannot communicate, and you get 502 Bad Gateway.

Verify with:

docker network ls | grep media
# Should show ONE network named "media", not "jellyfin_media"

If you see project-prefixed duplicates, fix the Compose file to use external: true.


Network Segmentation: The Homelab DMZ Pattern

Security through network segmentation is not just for enterprises. In a homelab, the principle is simple: group containers by trust level and sensitivity, and use Docker networks as the boundary. If one segment gets compromised, the blast radius stops at the network edge.

The Five-Zone Model

Zone Trust Level Example Services Network Name
Edge Exposed to internet Traefik, Authelia, CrowdSec proxy
Public Internet-facing apps Nextcloud, Jellyfin, Immich public
Internal LAN-only services Sonarr, Radarr, Home Assistant, Pi-hole internal
Storage Databases and secrets MariaDB, PostgreSQL, Redis, Vaultwarden storage
Management Admin tools Portainer, Uptime Kuma, Grafana, SSH mgmt

Rules of engagement:

  1. Edge can talk to everything — Traefik must route to all zones. Accept this risk and harden Traefik with Authelia SSO and rate-limiting middleware.
  2. Public apps can talk to Storage — Nextcloud needs MariaDB. Jellyfin doesn’t need Vaultwarden. Be specific: attach only the database that app actually needs.
  3. Internal services stay internal — Sonarr and Radarr should never be exposed to the internet. They communicate through the internal network only.
  4. Storage is silent — No service on the storage network should have outbound internet access. Use internal: true on the network definition to block external connectivity:
networks:
  storage:
    driver: bridge
    internal: true   # Blocks all outbound internet from this network
  1. Management is separate — Portainer and SSH containers should live on mgmt, accessible only via VPN or local LAN. Never expose Portainer’s dashboard directly to the internet, even behind Traefik with authentication.

Firewall Considerations

Docker manipulates iptables rules directly, which creates a well-known conflict: UFW or firewalld rules you write on the host are silently bypassed by Docker’s own iptables entries. Docker inserts its rules at a higher priority than user-defined rules, meaning a container bound to -p 8080:8080 is accessible on 0.0.0.0:8080 regardless of what UFW says.

The fix for UFW users:

# /etc/docker/daemon.json
{
  "iptables": false
}

Setting iptables: false tells Docker to stop manipulating iptables. You then manage all firewall rules through UFW directly. The downside: Docker’s inter-container communication and outbound NAT stop working unless you manually configure iptables rules. This is an advanced configuration — for most homelabbers, the simpler approach is binding containers to 127.0.0.1 instead of 0.0.0.0 and letting Traefik handle external access.

# Instead of this:
ports:
  - "8080:8080"

# Do this — only Traefik on the Docker network can reach it:
ports:
  - "127.0.0.1:8080:8080"

With port binding restricted to 127.0.0.1, the service is inaccessible from your LAN or the internet. Only Traefik (on the same Docker bridge) can route traffic to it. This is the single highest-impact security change you can make to a Docker homelab.


Real-World Architecture: A Complete Homelab Network

Here is a complete five-zone network architecture for a typical homelab running 20+ containers. Study the pattern, adapt it to your stack.

┌─────────────────────────────────────────────────────────┐
│                      INTERNET                            │
│                                                         │
│                   Ports 80/443                           │
│                                                         │
│  ┌──────────────────────┼──────────────────────────┐     │
│                EDGE (proxy)                             │
│    ┌─────────┐  ┌──────────┐  ┌──────────┐            │
│     Traefik    Authelia    CrowdSec             │
│    └────┬────┘  └──────────┘  └──────────┘            │
│  └───────┼─────────────────────────────────────────┘     │
│           routes to all zones                            │
│  ┌───────┼─────────────────────────────────────────┐     │
│                  PUBLIC (public)                       │
│    ┌────┴────┐  ┌──────────┐  ┌────────┐              │
│    Nextcloud   Jellyfin    Immich               │
│    └────┬────┘  └──────────┘  └───┬────┘              │
│  └───────┼─────────────────────────┼──────────────┘     │
│                                                        │
│  ┌───────┼─────────────────────────┼──────────────┐     │
│                 STORAGE                             │
│    ┌────┴────┐  ┌──────────┐  ┌──┴────────┐         │
│     MariaDB     Redis      PostgreSQL          │
│    └─────────┘  └──────────┘  └───────────┘         │
│           internal: true (no internet)              │
│  └──────────────────────────────────────────────┘     │
│                                                        │
│  ┌──────────────────────────────────────────────┐     │
│                INTERNAL (internal)                   │
│    ┌────────┐ ┌────────┐ ┌───────┐ ┌────────┐      │
│     Sonarr   Radarr  Pi-hole   HA          │
│    └────────┘ └────────┘ └───────┘ └────────┘      │
│  └──────────────────────────────────────────────┘     │
│                                                        │
│  ┌──────────────────────────────────────────────┐     │
│              MANAGEMENT (mgmt)                       │
│    ┌──────────┐ ┌───────────┐ ┌────────────┐       │
│     Portainer Uptime Kuma   Grafana          │
│    └──────────┘ └───────────┘ └────────────┘       │
│           Access: VPN / local LAN only              │
│  └──────────────────────────────────────────────┘     │
└─────────────────────────────────────────────────────────┘

This architecture is not theoretical. It runs in production homelabs and has been battle-tested across dozens of setups. The internal: true flag on the storage network means even if an attacker escapes a container on the public zone, they cannot exfiltrate your database contents to the internet — the network simply has no route out.


Common Docker Networking Mistakes (and Fixes)

1. Using localhost Between Containers

The mistake:

environment:
  - DATABASE_URL=mysql://localhost:3306/mydb

Why it fails: Inside a container, localhost means the container itself. Unless MySQL is running in the same container (it should not be), localhost:3306 is an empty port.

The fix: Use the service name from your Compose file — that is what Docker’s embedded DNS resolves:

environment:
  - DATABASE_URL=mysql://db:3306/mydb

2. Exposing Ports You Don’t Need

The mistake: Every service gets ports: - "XXXX:XXXX" in Compose.

Why it fails: Each exposed port is an attack surface. A homelab with 20 services, each exposing its port to 0.0.0.0, has 20 entry points an attacker can scan and probe. Your LAN is not a trusted network — smart TVs, IoT devices, and guest Wi-Fi clients all share it.

The fix: Only Traefik should expose ports to the host. Every other service communicates over Docker networks:

# Good — only Traefik gets host ports
services:
  traefik:
    ports:
      - "80:80"
      - "443:443"

  app:
    # No ports block — Traefik routes to it via Docker network
    networks:
      - proxy

Need to see every port your homelab is exposing? Run docker ps --format 'table {{.Names}}\t{{.Ports}}' and audit every 0.0.0.0:XXXX entry. For most services, the answer is 127.0.0.1:XXXX:XXXX.

3. Forgetting external: true on Shared Networks

The mistake: Multiple Compose files reference the same network name without external: true. Docker Compose creates <project>_networkname networks instead of joining the shared one.

The fix: Create networks once, then reference them with external: true everywhere else:

docker network create proxy
docker network create media
docker network create storage --internal
docker network create internal
docker network create mgmt

Then in every Compose file:

networks:
  proxy:
    external: true
  media:
    external: true

4. Macvlan for Every Container

The mistake: “I want every container to have its own LAN IP so I can access them directly.”

Why it fails: Macvlan is a specialist tool. Using it for every container wastes IP addresses, breaks Docker DNS, and prevents host-container communication. Docker’s embedded DNS does not work across macvlan networks — you are back to hardcoding IPs.

The fix: Use macvlan for the one or two containers that genuinely need it (Home Assistant, Pi-hole with DHCP). Put everything else on user-defined bridges with Traefik routing.


FAQ

Q: Should I put all my containers on one network or split them into multiple networks?

Start with one user-defined bridge for everything (the Compose default). Split into separate networks when: (a) you have more than 10 containers, (b) you need to prevent specific containers from communicating, or (c) you are exposing any container directly to the internet. The five-zone model above is the target state, not the starting point.

Q: Can Traefik route to a container on a macvlan network?

No. Traefik communicates over Docker’s virtual bridge, and macvlan operates at a different layer. If a container is on macvlan, Traefik cannot reach it through Docker networking. Workaround: give the container a second interface on a user-defined bridge (Docker supports multi-network containers) and have Traefik route through that.

Q: Does Docker Compose’s network_mode: host disable all other networking?

Yes. A container with network_mode: host cannot be attached to any other Docker network. You cannot mix host networking with user-defined bridges. Choose one per container.

Q: How do I give a container a static IP on a user-defined bridge?

Docker supports static IP assignment on user-defined bridges with --subnet and per-container ipv4_address:

networks:
  custom:
    driver: bridge
    ipam:
      config:
        - subnet: 10.99.0.0/24

services:
  app:
    networks:
      custom:
        ipv4_address: 10.99.0.100

Avoid this unless you have a concrete reason — it defeats Docker’s dynamic IP management and creates a single point of configuration drift.

Q: Can containers on different Docker hosts communicate over a bridge?

Not natively. Docker bridge networks are host-local. For cross-host container communication, you need Docker Swarm with overlay networks, or a Layer 3 solution like Tailscale. For most homelabbers running on a single Proxmox node, this is a non-issue.

Q: What happens when two Compose files define the same network?

If both define it as external: true, they reference the same pre-existing network. If one defines it inline and the other uses external: true, they land on different networks and the containers cannot communicate. Consistent external: true usage across all Compose files is the solution.

Q: How do I block internet access for a specific container?

Use the internal: true flag on the network:

networks:
  no-internet:
    driver: bridge
    internal: true

Any container on this network can communicate with other containers on the same network but has no route to the internet. This is ideal for databases and sensitive services.


Conclusion: The Clean Setup Checklist

Docker networking rewards discipline. Start with the default, then migrate toward segmentation as your homelab grows. Here is the checklist for a network architecture that scales:

  1. No containers on the default bridge network — docker network inspect bridge | grep Name should return only infrastructure containers Docker itself placed there.
  2. User-defined bridges for every service group — at minimum, separate proxy, apps, and storage.
  3. Traefik is the only container exposing host ports — everything else communicates over Docker networks.
  4. Databases bound to internal: true networks — no outbound internet from your storage layer.
  5. Portainer and admin tools on mgmt, accessed via VPN — never exposed to the internet.
  6. 127.0.0.1 port bindings where possible — only Traefik needs 0.0.0.0 exposure.

If your current Docker setup looks nothing like this, do not panic. Move one service group at a time. Start by creating a proxy network and moving Traefik onto it. Next cycle, create storage and move your databases. The goal is continuous improvement, not a weekend rewrite.

Next reads to level up your Docker infrastructure: