Docker Homelab
Flat isometric illustration of three rounded white service blocks with port openings sitting directly on one flat grey plate, beside two rows of network jacks set into the same plate.
networking

Docker Compose Host Networking (network_mode: host)

How network_mode host works in Docker Compose, why published ports stop working, what it breaks for DNS and Traefik, and when to pick it anyway.

By Docker Homelab Editorial · ·Updated August 22, 2026 · 8 min read

network_mode: host is a single line of YAML that changes more about a container than almost any other setting in a Compose file. It is also the line people paste in when something will not work, without knowing what it turns off. This guide explains what host networking actually does, the rules Compose enforces around it, the four things that silently stop working, and the short list of services where it is the right answer.

What network_mode: host Actually Does

Every normal Docker container gets its own network namespace: its own interfaces, its own routing table, its own set of ports. A virtual ethernet pair connects that namespace to a bridge on the host, and published ports are NAT rules that forward host traffic into it.

Host mode removes the namespace. The container’s processes bind directly to the host’s interfaces and share the host’s routing table, its ports, and its IP addresses. There is no bridge, no veth pair, no NAT, and no address translation to reason about.

services:
  pihole:
    image: pihole/pihole:latest
    network_mode: host
    volumes:
      - ./etc-pihole:/etc/pihole
    restart: unless-stopped

The Pi-hole process binding port 53 inside that container is binding port 53 on the host, on every interface the host has. Not forwarded to. Listening on.

The practical consequence is the one people miss: a host-networked container is no longer a container as far as the network is concerned. Anything that reasons about containers by network identity, including Docker’s own DNS, stops applying to it.

The Rules Compose Enforces

Three constraints come straight from the Compose specification and the Docker Engine, and hitting them is the usual first surprise.

You cannot combine network_mode with networks. Compose rejects the file outright rather than guessing which one you meant:

services:
  app:
    image: myapp:latest
    network_mode: host
    networks:          # error: "network_mode" and "networks" cannot be combined
      - backend

There is no partial version of this. A service is either in the host’s namespace or on Docker networks, never both. If a service needs to reach a database on a private network and needs the host’s network stack, the design has to change, not the YAML.

Published ports are ignored. ports: entries do nothing in host mode. Docker’s host network documentation is explicit that port mapping does not take effect and the flags are discarded with a warning. The container is already on the host’s ports; there is nothing to forward.

services:
  app:
    image: myapp:latest
    network_mode: host
    ports:
      - "8080:80"   # ignored - the app is on whatever port it binds internally

This is a genuinely dangerous silent failure, because the YAML still reads as though the service is on 8080. It is on port 80, or whatever the image’s process actually binds, and no amount of editing the left-hand number will change that. To move the port you have to change the application’s own configuration, usually through an environment variable.

expose is inert; links is a hard failure. expose only advertises a port to other containers on a Docker network, and there is no such network here, so it does nothing at all. links is not merely ignored — the Docker Engine refuses to create the container, with conflicting options: host type networking can't be used with links. This would result in undefined behavior. Inheriting an old Compose file that still carries links is the usual way people meet that message.

What Silently Breaks

Service-name DNS stops resolving. Docker’s embedded DNS server is a feature of user-defined networks. A host-networked container resolves names through the host’s /etc/resolv.conf instead, so postgres://db:5432 fails with “name or service not known”. The container has to use 127.0.0.1 or the host’s LAN address, and the database has to publish its port to the host for that to work at all — which reopens the exposure you were avoiding.

The reverse also fails. Other containers on a bridge network cannot reach a host-networked service by its service name. They need the host’s address, which from inside a bridge network is reachable with:

services:
  app:
    image: myapp:latest
    extra_hosts:
      - "host.docker.internal:host-gateway"

Reverse proxies stop routing to it. Traefik discovers containers through the Docker API and routes to their network address. A host-networked container has no container network address, so label-based discovery has nothing to point at. The same applies to any proxy that resolves upstreams by container name.

The workaround is a fixed upstream pointing at the host’s LAN address and the port the process actually binds. In Traefik that means dropping this one service out of the Docker provider and declaring it in the file provider instead:

# traefik/dynamic/hass.yml, loaded by providers.file
http:
  routers:
    hass:
      rule: "Host(`hass.example.com`)"
      service: hass
  services:
    hass:
      loadBalancer:
        servers:
          - url: "http://192.168.1.10:8123"

Use the host’s LAN address, not 127.0.0.1 — loopback inside a bridge-networked Traefik container is Traefik’s own loopback, not the host’s. In Nginx Proxy Manager the equivalent is setting the proxy host’s forward hostname to that same LAN IP instead of a container name. Either way the address is now hardcoded and stops following the host, which is the automatic part you gave up. The label-driven setup this replaces is in the Traefik with Docker Compose guide and the Nginx Proxy Manager tutorial.

Port conflicts become host problems. Two host-networked containers that both want port 8080 will not start; the second one fails at bind time with “address already in use”. Worse, the conflict can be with something that is not a container at all. The canonical case is DNS: systemd-resolved binds 127.0.0.53:53 on most Ubuntu and Debian systems, and a host-networked Pi-hole collides with it immediately. Confirm it with sudo ss -tulpn | grep ':53' before assuming anything else, then either free the port by setting DNSStubListener=no in /etc/systemd/resolved.conf, which keeps resolved doing its other work, or stop the service outright. The blunt version, including the /etc/resolv.conf repair the host needs afterwards so it can still resolve names, is in the Pi-hole Docker Compose guide.

Interface scoping disappears. On a bridge network you can publish a port to loopback only, with 127.0.0.1:8080:80, keeping a service off the LAN entirely. Host mode has no equivalent. Whatever address the containerized process binds is what you get, and most images bind 0.0.0.0. A management interface you assumed was private is now answering on every interface the host owns, including any WAN-facing one. If the image does not offer a bind-address setting, host mode has no way to constrain it and a host firewall becomes the only control left.

Where Host Mode Is Genuinely Correct

Four categories justify it, and they share a trait: the service needs to see traffic that NAT destroys.

Layer 2 broadcast and DHCP. DHCP works by broadcasting to an address no router forwards. A bridge-networked container never sees those packets. Pi-hole acting as a DHCP server, or any service that has to answer a broadcast, needs either host mode or a macvlan network.

Multicast service discovery. mDNS, SSDP, and similar protocols rely on multicast groups that do not cross a NAT boundary. Home Assistant finding a Chromecast, Jellyfin answering DLNA probes on UDP 1900, and printer discovery all fall into this group. The Home Assistant section of the home server container roundup is the concrete version of this trade-off.

Very large or unpredictable port ranges. Publishing a range like 10000-20000:10000-20000 creates a forwarding rule per port and, on older configurations using the userland proxy, a process per port. Host mode makes the whole range work with no per-port cost. VoIP and some game servers land here.

Raw packet rate. Removing NAT removes a per-packet cost. For a typical homelab stack this is irrelevant; the difference only becomes measurable at rates a home server rarely reaches. Choosing host mode for performance alone, on a stack serving a handful of users, is trading real isolation for a benefit you will not observe.

Bridge, Host, and Macvlan Compared

Bridge (default)network_mode: hostMacvlan
Container IPPrivate, per-networkNone of its own; uses host IPsIts own IP on your LAN
ports: worksYesIgnoredIgnored (all ports open on its IP)
Service-name DNSYesNoYes, between containers on that network
Sees LAN broadcastNoYesYes
Port conflictsOnly on published host portsHost-wide, with non-container services tooNone; separate IP per container
Reverse-proxy discoveryWorksBreaksBreaks
Host can reach itYesYesNo, without a shim interface
IsolationFullNoneFull from the host, none from the LAN

The row that decides most cases is the last-but-one. If the goal is “this service needs its own presence on the LAN”, macvlan does that without surrendering the host’s port space, and it is covered in the Docker macvlan networking guide. Host mode is the right choice when the service needs the host’s identity on the network, not one of its own.

Two Modes People Confuse With It

network_mode accepts more than host, and two of the other values solve problems that host mode is often misapplied to.

services:
  vpn:
    image: qmcgaw/gluetun
    ports:
      - "8080:8080"

  downloader:
    image: some/downloader
    network_mode: "service:vpn"   # shares the vpn container's namespace

service:<name> puts one container in another container’s network namespace. This is the standard pattern for routing an application through a VPN container: the application has no network of its own, so it cannot leak around the tunnel. Ports are published on the VPN container instead.

container:<id> does the same thing against a container that Compose does not manage. none disables networking entirely, which is a reasonable default for a job that only touches mounted volumes.

Checking What You Actually Got

Four commands settle almost every question, and the first one is the fastest way to confirm the mode took effect:

docker inspect -f '{{.HostConfig.NetworkMode}}' <container>

# What is holding the port on the host
sudo ss -tulpn | grep :53

# Bridge-networked containers: their address and network
docker network inspect bridge

# What the container itself thinks its interfaces are
docker exec <container> ip addr

In host mode, ip addr inside the container shows the host’s interfaces, because they are the host’s interfaces. That output is the clearest confirmation that the namespace is shared. A container still showing a lone eth0 on a 172.x address did not get host mode, usually because a networks: key elsewhere in the file quietly won.

One platform caveat worth knowing before you plan around it: host networking is a Linux capability. Docker Desktop on macOS and Windows runs containers inside a Linux virtual machine, so “the host” is that VM rather than your laptop, and support there is comparatively recent and opt-in. Rootless Docker is a second special case, where the shared namespace belongs to the rootless helper rather than to the real host. Both are documented on Docker’s host network page, and both mean a Compose file that behaves one way on a homelab server can behave differently on a workstation.

A Short Decision Rule

Use bridge networking with published ports by default; it is the only mode where isolation, DNS, and proxy discovery all work. Reach for network_mode: host when the service must see broadcast or multicast traffic, or when it needs a port range too large to forward sensibly. Reach for macvlan when the service needs its own LAN address rather than the host’s. And when a service is host-networked, treat every port it binds as published to your whole network, because that is exactly what it is.

If your stack is still taking shape, the Docker Compose networking guide covers the bridge-mode fundamentals this article assumes, and the first self-hosted stack walkthrough builds one from an empty directory.

Sources

  1. Docker Docs — Host network driver
  2. Docker Docs — Compose file reference, network_mode
  3. Docker Docs — Networking overview
  4. Pi-hole Docker Documentation

Related