To put a Docker Compose service on the host network, add network_mode: host under that service and delete its ports:, networks: and links: keys. The container then shares the host’s IP addresses and ports directly: no bridge, no NAT, no service-name DNS. It is the Compose equivalent of docker run --network host.
That one line changes more about a container than almost any other setting in a Compose file, and it usually gets pasted in when something will not work, without anyone checking what it turns off.
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. Docker’s Compose networking page states the cost plainly: such a container “can access all host ports and observe all network traffic on the host.”
How to Switch a Compose Service to Host Networking
Six steps move an existing bridge-networked service to host mode without leaving dead config behind. Start from a typical service that publishes 8080:80 and joins a private network:
services:
app:
image: myapp:latest
ports:
- "8080:80"
networks:
- backend
-
Read the right-hand side of each port mapping. In
8080:80the process listens on 80, so 80 is the port it will occupy on the host. -
Check that the port is free.
sudo ss -tulpn | grep ':80 'should print nothing; otherwise the container fails with “address already in use”. -
Swap the networking keys. Add
network_mode: hostand deleteports:,networks:,expose:and anylinks::services: app: image: myapp:latest network_mode: host # now listening on host port 80 -
Change the port inside the application if 80 is wrong. Host mode has no remapping; only the application’s own listen setting moves it.
-
Recreate the container.
docker compose up -drecreates any container whose configuration changed;--force-recreatedoes it unconditionally.docker compose restartdoes not apply Compose file changes, so it will not switch the mode. -
Repoint everything that used the service name. Upstreams that said
app:80now need the host’s LAN IP, and anythingappreached by name, such asdb, needs a port the host can reach.
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.
The Compose file reference is stricter, saying port mapping with host mode “causes a runtime error”. A September 2026 check on Compose 5.5.0 and Docker 29.1.3 found the looser behavior: Compose accepted the file and the daemon printed WARNING: Published ports are discarded when using host network mode. Delete the line either way; a leftover 8080:80 starts publishing again the day someone removes network_mode: host.
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, and the Jellyfin Docker Compose guide walks through the DLNA case.
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.
Every Value network_mode Accepts
Compose’s network_mode accepts five values: bridge, host, none, service:{name} and container:{name}. Leave the key out and the service joins the project’s own <project-name>_default network, which is what most stacks want. Setting any of the five makes networks: illegal on that service, because Compose rejects a file containing both.
| Value | Container’s networking | ports: | Typical use |
|---|---|---|---|
| (omitted) | Own IP on <project-name>_default, service-name DNS | Works | Almost everything |
bridge | Docker’s default bridge, where service names do not resolve | Works | Legacy files |
host | Host’s interfaces, no IP of its own | Discarded | Broadcast, multicast, big port ranges |
none | Loopback only | Nothing to publish on | Volume-only jobs |
service:{name} | Another service’s namespace | Refused | VPN sidecar |
container:{name} | An existing container’s namespace | Refused | Containers outside the project |
The two namespace-sharing 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. Put ports: on the downloader and the daemon refuses with conflicting options: port publishing and the container type network mode, the error VPN-sidecar setups hit most often. The networking overview lists --expose, --dns, --hostname and --add-host as unsupported in this mode too.
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.
Do not confuse any of these with build: network: host, which only sets networking for RUN instructions during the image build; the Compose build reference accepts default, none or host there. The running container still needs its own network_mode: host.
Bridge, Host, and Macvlan Compared
| Bridge (default) | network_mode: host | Macvlan | |
|---|---|---|---|
| Container IP | Private, per-network | None of its own; uses host IPs | Its own IP on your LAN |
ports: works | Yes | Ignored | Ignored (all ports open on its IP) |
| Service-name DNS | Yes | No | Yes, between containers on that network |
| Sees LAN broadcast | No | Yes | Yes |
| Port conflicts | Only on published host ports | Host-wide, with non-container services too | None; separate IP per container |
| Reverse-proxy discovery | Works | Breaks | Breaks |
| Host can reach it | Yes | Yes | No, without a shim interface |
| Isolation | Full | None | Full 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.
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 it was never recreated after the edit, or because the line landed under a different service. Portainer shows the same fact from the outside: a host-networked container lists no network and no published ports, which the Portainer setup guide flags as expected rather than broken.
Two platform caveats are 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 Desktop supports host mode only from version 4.34, as an opt-in setting limited to layer 4. Rootless Docker is the second special case: before Engine 29.5, rootless mode namespaced host networking inside RootlessKit, so a host-networked container’s ports were not reachable from the real host until the 29.5.0 release fixed it. Either way, a Compose file that behaves one way on a homelab server can behave differently on a workstation or an older rootless install.
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.
FAQ
what is the docker compose equivalent of docker run —network host
network_mode: host on the service is the direct equivalent of docker run --network host. The same mapping holds for the other modes: --network none becomes network_mode: none, and --network container:vpn becomes network_mode: "container:vpn", or network_mode: "service:vpn" when the target is defined in the same Compose file.
how do i change the port of a container using network_mode host
You change it inside the application, not in Compose. Host mode discards ports:, so the container listens on whatever port its process binds. Set the application’s own listen-port option, usually an environment variable or config file named in the image’s documentation, recreate the container, and confirm the new port with sudo ss -tulpn.
can a docker compose service use network_mode host and networks at the same time
No. Compose rejects any file in which one service sets both network_mode and networks. If a host-networked service must reach a private database, publish the database port on 127.0.0.1 and connect to 127.0.0.1, which the shared namespace can reach. Alternatively, move the service to macvlan, which can sit alongside the private network.
does network_mode host work on docker desktop for mac and windows
Yes, from Docker Desktop 4.34, but only after you sign in and enable host networking under Settings, Resources, Network, then apply and restart. It is limited to layer 4, so TCP and UDP work and lower-level protocols do not. It is also incompatible with Enhanced Container Isolation, and Windows containers are not supported.