network_mode: host Not Working on Docker WSL2: Fix
network_mode: host in docker-compose.yml either refuses to start with the exact error "host" network_mode is incompatible with port_bindings, or it starts and then quietly does nothing reachable from Windows on Docker Desktop with WSL2. Both symptoms are the same story: host networking on Docker Desktop is opt-in and only became generally available in version 4.34, and even when it is enabled it attaches to Docker's utility VM — not to your WSL2 distro and not to Windows. That mismatch is why the 22k-view Stack Overflow question "running network mode host on windows 10 with wsl2" is still unanswered with a definitive fix.
I hit this on a Windows 10 box that was pinned to Docker Desktop 4.19 by an IT policy. The compose file worked on the Linux CI runner, failed locally with the port_bindings error, and after I removed ports: it "worked" in the sense that the container started — and then nothing on the host could reach it. This post is the decision path I use now: what host mode actually does, why Docker Desktop and WSL2 break it, and the four fixes in order of how often they are the right one.
TL;DR
- The compose error is a contradiction in terms.
"host" network_mode is incompatible with port_bindingsmeans you declared bothnetwork_mode: hostandports:— in host mode there is no port mapping, so compose rejects the pair. Remove one of them. - Docker Desktop only learned host networking in 4.29 (beta) and 4.34 (GA). Before that it simply was not supported on Mac/Windows — the flag was accepted and then ignored or misbehaved. Check your version before debugging anything else.
- Even on 4.34+, host mode is opt-in: Settings → Resources → Network → Enable host networking, signed in to a Docker account. It is layer-4 (TCP/UDP) only, does not work with Enhanced Container Isolation, and Linux containers only.
- The namespace trap: on WSL2 the engine runs in the
docker-desktoputility distro, sonetwork_mode: hostshares that namespace —localhostinside the container is not your Ubuntu distro's localhost and not Windows' localhost. - For 95% of services, bridge +
ports:is the correct fix — not host mode. Usehost.docker.internalwhen the container must reach a service on the Windows host. - If you genuinely need host mode (avahi, packet capture, dynamic port ranges), run the Docker Engine natively inside your WSL2 distro instead of Docker Desktop — then host mode shares your distro's real kernel network stack.
What "network_mode: host" actually does
Per the Docker host network driver docs, host mode makes the container share the host's networking namespace: it does not get its own IP address, and because there is no per-container mapping, the -p/--publish/-P options are ignored — the daemon prints WARNING: Published ports are discarded when using host network mode. On native Linux that is exactly what you want for a service that must bind arbitrary or many ports, or that needs to see real host interfaces.
| Symptom | Root cause | Fastest fix |
|---|---|---|
"host" network_mode is incompatible with port_bindings | network_mode: host + ports: in the same service | Remove ports: or drop host mode (Fix 2) |
| Container starts, port unreachable from Windows/WSL2 | Docker Desktop < 4.34, or host networking not enabled in Settings | Upgrade + enable (Fix 1) |
curl localhost:8080 fails inside WSL2 but works from Windows | Host mode shares the docker-desktop utility VM namespace, not your distro's | Bridge + ports (Fix 2) |
| Container must reach a service on the Windows host | Host networking does not expose the Windows host's interfaces | host.docker.internal (Fix 3) |
Why compose rejects it: the port_bindings error
The exact error string is compose's validation, and it fires at docker compose up time. This is the file that produces it:
services:
api:
image: my-api:1.4.0
network_mode: "host"
ports:
- "8080:8080" # illegal: host mode has no port mapping
Compose refuses with this exact message:
$ docker compose up
Error response from daemon: "host" network_mode is incompatible with port_bindings
It is a specification contradiction, not a daemon bug: host mode means the container binds directly on the host's interfaces, so there is nothing for ports: to translate. The fix is either to delete the ports: block (correct on Linux, where the service then binds the host port itself), or — almost always the better move — delete network_mode: host and keep the ports (Fix 2 below). Note that Docker Compose's own issue tracker documents the same conflict in docker/compose#3442, and the same "works on Linux, dies on Docker Desktop" split shows up in the 54k-view Stack Overflow thread.
The other failure: Docker Desktop had no host mode at all
If you are on a Windows 10 + WSL2 machine that cannot upgrade Docker Desktop (IT pinning, old hardware), the port_bindings error is only half the story. Historically, host networking was not supported on Docker Desktop for Mac or Windows at all — the driver docs listed only "Docker Engine on Linux". Docker changed that in two steps:
| Version | Date | Host networking status |
|---|---|---|
| Docker Desktop 4.29 | 2024-04-08 | Beta — announced for Mac and Windows, requires Docker account authentication |
| Docker Desktop 4.34.0 | 2024-08-29 | Generally available — documented in the host driver docs |
Before 4.34, --network host on Docker Desktop was at best ignored and at worst attached the container to the hypervisor/utility VM's network — which is why Home Assistant's install guide has a long history of "works on Linux, silently broken on Docker Windows" threads. Check your version first: docker version prints the server version (e.g. 24.0.7 for Docker Desktop 4.29+), or look at Docker Desktop → About. If you are below 4.34, upgrade before touching your compose file.
Fix 1: turn on host networking (Docker Desktop 4.34+)
On a current Docker Desktop, host mode is a switch, not a default. The official steps from the host driver docs:
- Sign in to your Docker account in Docker Desktop (host networking is gated on authentication).
- Open Settings.
- Under the Resources tab, select Network.
- Check Enable host networking.
- Select Apply and restart.
Once enabled, the official smoke test is a netcat listener in host mode, reachable from the host's own localhost:
# terminal 1 — inside a host-mode container
docker run --rm -it --net=host nicolaka/netshoot nc -lkv 0.0.0.0 8000
# terminal 2 — on the Windows host (stock Windows has no nc — use PowerShell)
Test-NetConnection localhost -Port 8000 # TcpTestSucceeded : True
# or: curl http://localhost:8000
Stock Windows does not ship nc, so use PowerShell's Test-NetConnection (or curl) for the host-side check; install Nmap's ncat via winget install Nmap.Nmap only if you specifically want the classic netcat syntax. Three documented limits matter before you rely on this. Host networking on Docker Desktop works on layer 4 only — TCP and UDP, nothing below (no raw ICMP, no ARP tricks). It is incompatible with Enhanced Container Isolation — the two settings contradict each other, so containers lose host-network access when ECI is on. And it is Linux containers only; Windows containers do not support host mode.
One more current gotcha from Docker's own release notes (Docker Desktop 4.43): there is a known incompatibility between the host networking feature and the most recent WSL 2 Linux kernel — if host mode breaks right after a wsl --update, Docker's note says to downgrade WSL 2 to kernel 2.5.7. Microsoft ships wsl --update --rollback to revert to the previous kernel, or you can install the 2.5.7 kernel package manually.
Why localhost still points at the wrong machine
This is the trap that makes the original Stack Overflow question so persistent. Docker Desktop for Windows with the WSL2 backend runs the Docker Engine inside a dedicated WSL2 distribution called docker-desktop — not inside your Ubuntu distro. The networking docs describe the utility VM's role: outbound container traffic goes through a virtual adapter (typically 192.168.65.x), and port publishing is handled by Docker's backend process on the Windows side.
Consequence: network_mode: host shares the utility VM's network namespace. "localhost" means three different things on this machine — Windows' localhost, your Ubuntu distro's localhost, and the docker-desktop distro's localhost — and a host-mode container only ever sees the third. Verify it yourself:
# inside your WSL2 Ubuntu distro
docker run --rm --network host alpine ip addr # shows the docker-desktop VM's interfaces
docker inspect <container> --format '{{.HostConfig.NetworkMode}}' # prints: host
You will see 192.168.65.x addresses — Docker's virtual subnet — not your distro's eth0. The same namespace confusion is exactly what we mapped for Tailscale's SSL_ERROR_RX_RECORD_TOO_LONG: a service that assumes "localhost means the box I am on" breaks the moment a VM boundary sits in between.
Fix 2: bridge + ports (the recommended default)
For a web API, database, or any service with a bounded port set, host mode is the wrong tool on Docker Desktop. Bridge networking plus an explicit ports: mapping uses Docker's supported path: the backend forwards host ports into the VM and the container gets a normal private IP. This is the production-grade replacement for the failing compose file above:
services:
api:
image: my-api:1.4.0
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080" # bind localhost only — do not expose to the LAN
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/healthz"]
interval: 30s
timeout: 3s
retries: 3
Note the bind address: 127.0.0.1:8080:8080 publishes only on the Windows host's loopback, which is what local development wants. If you omit the address, 0.0.0.0 is implied and the port is reachable from the LAN — a firewall difference that matters on shared networks. The healthcheck keeps the service honest under restart policies, the same "fail fast, verify after" discipline we used in the nginx 499 client-closed-request write-up.
Fix 3: host.docker.internal for host-side services
When the actual requirement is "the container must reach a service running on the Windows host" — a database, a license server, an SSH tunnel — you do not need host mode. Docker Desktop registers a special DNS name, host.docker.internal, that resolves to the host's internal address from inside any container. The desktop networking docs recommend it explicitly over guessing the host's changing IP:
# from inside a container; service listens on the Windows host
docker run --rm curlimages/curl -fsS http://host.docker.internal:8080/healthz
Important asymmetry: host.docker.internal is a Docker Desktop convenience — it is not automatically defined when you run the Engine natively inside WSL2 (Fix 4). In that setup, point the container at the WSL2 virtual switch IP instead, or add an extra_hosts entry.
Fix 4: native Docker Engine inside WSL2 (real host mode)
If you actually need host semantics — avahi/mDNS discovery, tcpdump on a real interface, a service that binds a large or dynamic port range — stop using Docker Desktop for that workload and run the Docker Engine inside your WSL2 distro. The distro is a real Linux kernel, so host mode behaves exactly as it does on a Linux server: the container shares your distro's network namespace, and localhost finally means the same thing everywhere inside WSL2.
# inside WSL2 Ubuntu — official Docker convenience script
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker "$USER"
# start a new shell, then:
docker run --rm --network host alpine ip addr # now shows YOUR distro's eth0
Trade-offs are real: you manage the engine yourself (sudo systemctl enable --now docker or sudo service docker start), Windows-side port publishing no longer applies, and the legacy route to expose a WSL2 port to the LAN is a Windows-side portproxy rule:
# PowerShell (admin) — forward WSL2 port 8080 to the LAN
netsh interface portproxy add v4tov4 listenport=8080 listenaddress=0.0.0.0 connectport=8080 connectaddress=<wsl2-ip>
# get from inside the distro: ip addr show eth0 (or hostname -I )
# Windows Firewall blocks inbound by default — allow TCP 8080 from the LAN only
netsh advfirewall firewall add rule name="WSL2 8080" dir=in action=allow protocol=TCP localport=8080 remoteip=localsubnet
Three portproxy caveats. First, the portproxy rule is not enough: Windows Firewall will still drop the inbound connection unless the advfirewall rule above exists — the example scopes it to the local subnet with remoteip=localsubnet; replace that with a specific source CIDR if your clients are on another network. Second, listenaddress=0.0.0.0 exposes the service to the whole LAN; use your Windows host's LAN IP if you want to restrict it. Third, the WSL2 distro's eth0 address is DHCP-assigned and changes on every reboot, so the connectaddress goes stale — either re-add the rule with the current IP after boot, or enable WSL mirrored networking (networkingMode=mirrored under [wsl2] in %USERPROFILE%\\.wslconfig, WSL 2.0.4+) so the distro keeps the host's interfaces and the proxy address is stable. Also confirm the IP Helper service (iphlpsvc) is running, since portproxy depends on it.
Verify the fix
- Confirm the version:
docker version— server version must be 4.34+ (Docker Engine 27.x line) for Docker Desktop host mode. - Confirm the mode:
docker inspect <container> --format '{{.HostConfig.NetworkMode}}'printshost,bridge, or your network name — it must match what you intended. - Bridge case:
docker port <container>lists the published mapping (e.g.8080/tcp -> 127.0.0.1:8080); thencurl http://localhost:8080/healthzfrom Windows and from inside the WSL2 distro. - Host case (4.34+): run the netshoot listener from Fix 1 and connect with
Test-NetConnection localhost -Port 8000(orcurl) from the host; on the container side,ip addrmust show the interface set you expect (utility VM vs your distro). - Logs: if the container still cannot be reached, read
docker logs <container>and confirm the app is binding0.0.0.0, not127.0.0.1inside the container — a loopback-only bind is unreachable even with correct port mapping.
When host mode is the wrong answer
Host mode on Docker Desktop is a compatibility feature with sharp edges, and the four fixes above are ordered by how often they should be your final state: prefer bridge + ports for anything with a fixed port list; use host.docker.internal for host-side dependencies; enable host mode only when a container genuinely needs the VM's network stack; and run a native engine in WSL2 when you need real Linux host semantics. The port_bindings error is compose doing its job — the contradiction is in the file, and the fix is choosing the networking model that matches your workload instead of fighting the wrapper.