Sitemap

Making host.docker.internal Work in WSL2

alex_ber
9 min readMay 8, 2025

Note: If you need only to make DNS resolution service inside docker container on WSL2 work without defining new entry there is much simpler solution https://alex-ber.medium.com/dns-resolution-service-inside-docker-container-on-wsl2-072a24d873f6

Introduction

When running Docker containers directly within WSL2 (i.e., without Docker Desktop managing the environment), a common challenge is enabling containers to communicate with services running on the Windows host, such as databases accessed via SSH tunnels. Docker Desktop conveniently provides the host.docker.internal DNS name, which resolves to an IP that allows containers to reach the host. However, this isn’t automatically available with a standalone Docker Engine setup in WSL2.

While --add-host=host.docker.internal:YOUR_HOST_IP in docker run commands is a workaround, a cleaner solution is to make host.docker.internal resolvable via DNS for all applications and containers within your WSL2 environment. This can be achieved by running a local DNS server (like Unbound) inside WSL2. Additionally, we can make host.docker.internal a convenient alias on Windows itself.

The Problems Encountered

Setting up this seemingly straightforward solution can hit several obstacles:

  1. Local DNS Server Unreachability with Mirrored Networking. Using WSL2’s networkingMode=mirrored can interfere with the operation of local DNS servers (like Unbound or Dnsmasq) listening on `127.0.0.1:53` within WSL2. Clients might receive “connection refused” errors.
  2. Docker Container DNS Misconfiguration. Even with a working DNS server in WSL2, Docker containers might fail to use it, often defaulting to public DNS servers which cannot resolve local custom names.
  3. Docker Embedded DNS Issues. Explicitly configuring Docker to use the correct local DNS path might still fail if Docker’s embedded DNS forwarder (on the docker container network gateway) isn’t running or accepting connections correctly, leading to “connection refused” or timeouts.
  4. WSL2 NAT Routing Complexity. When connecting from a container to the Windows host using the host’s IP on the WSL virtual network (e.g., 172.17.48.1), connections might time out due to issues routing return packets back to the container through WSL2’s NAT layer.

The Solution: NAT Mode, Unbound Config, Windows Hosts File, and Explicit Docker DNS

This multi-step solution addresses these issues to provide reliable name resolution and connectivity for host.docker.internal from within Docker containers running in WSL2.

Find Windows Host’s LAN IP Address

Open Command Prompt/PowerShell on Windows, run ipconfig.
Note the IPv4 Address and Default Gateway of your active network adapter, typically Ethernet adapter Ethernet, (e.g., 192.168.1.109). Default Gateway is typically your router’s IP address (e.g. 192.168.1.1)
Or open Power Shell:

#find all adapters with theire desciptions
Get-NetAdapter | Format-Table Name, InterfaceDescription
$adapterName = "Ethernet*" # Change this to your adapter name or pattern
#YOUR_WINDOWS_LAN_IP
Get-NetIPAddress -AddressFamily IPv4 -InterfaceAlias $adapterName | Select-Object IPAddress, InterfaceAlias
#YOUR_WINDOWS_DEFAULT_GATEWAY (optional)
Get-NetRoute | Where-Object {$_.DestinationPrefix -eq '0.0.0.0/0'} | Select-Object NextHop

Configure Windows Host for Convenience (Optional)

This step makes host.docker.internal work for applications running directly on Windows, pointing to the host’s primary LAN IP.

Edit Windows hosts file:

  • Open Notepadpp/Notepad (or similar) as Administrator.
  • Open C:\Windows\System32\drivers\etc\hosts.
  • Add the line (replace IP with your own):
192.168.1.109 host.docker.internal

Configure WSL2 Environment

This sets up the necessary WSL2 network mode and identifies key IP addresses.

  1. Modify WSL2 Network Configuration (Use NAT Mode):
    Disable the problematic networkingMode=mirrored.
  • Edit .wslconfig:
    Open C:\Users\<YourUser>\.wslconfig.
  • Remove/Comment Out Mirrored Mode:
    Ensure networkingMode=mirrored is removed or commented out under [wsl2].
[wsl2]
# networkingMode=mirrored
  • Restart WSL2:
    Exit from all WSL2 bash/terminals.
    From cmd/Power Shell run wsl --shutdown, that will restart your WSL2 distribution.

2. Disable `systemd-resolved` (Crucial for Unbound):

systemd-resolved` can conflict with Unbound on port 53 and interfere with /etc/resolv.conf.

  • Check Status: sudo systemctl status systemd-resolved
  • Stop and Disable:
sudo systemctl stop systemd-resolved
sudo systemctl disable systemd-resolved
  • Verify: Check the status again to ensure it's inactive (dead) and disabled.
  • Remove symlink:
sudo rm -f /etc/resolv.conf

3. Identify Key IP Addresses (Inside WSL2 Terminal):

  • Windows Host LAN IP (YOUR_WINDOWS_LAN_IP): already found at the very beginning, e.g. 192.168.1.109. This is the IP Unbound will resolve host.docker.internal to.
  • Windows Default Gateway (YOUR_WINDOWS_DEFAULT_GATEWAY): already found at the very beginning, e.g. 192.168.1.1. This is the IP Unbound will use for forwarding other DNS queries. Optional.
  • Docker Bridge Gateway IP (DOCKER_BRIDGE_IP): This is the IP containers will use as their DNS server.
DOCKER_BRIDGE_IP=$(ip addr show docker0 | grep "inet\s" | awk '{print $2}' | sed 's|/.*$||')
echo "Docker Bridge Gateway IP: ${DOCKER_BRIDGE_IP}"
# Example output: 172.17.0.1

Note these two IP addresses.

Install and Configure Unbound in WSL2

This sets up the local DNS server to resolve host.docker.internal to the host’s Windows Host LAN IP, which avoids WSL NAT return-path issues (see the problem encountered above).

  1. Install Unbound:
# Ensure DNS works temporarily for apt if needed
# echo "nameserver 8.8.8.8" | sudo tee /etc/resolv.conf
sudo apt update
sudo apt install unbound -y

2. Configure Unbound:

Edit or create your primary Unbound custom configuration file (e.g., /etc/unbound/unbound.conf.d/my-custom-settings.conf):

sudo nano /etc/unbound/unbound.conf.d/my-custom-settings.conf

Ensure it has the following content, replacing YOUR_WINDOWS_LAN_IP and DOCKER_BRIDGE_IP with the values you found. Adjust the Docker subnet (`172.17.0.0/16`) if necessary.

server:
# Verbosity level (0 is default, 3 is quite verbose for queries)
verbosity: 1

## Interface to listen on (0.0.0.0 for all, or specific IP)
# Listen on localhost AND the Docker bridge gateway IP
interface: 127.0.0.1
interface: 172.17.0.1 # DOCKER_BRIDGE_IP
port: 53
do-ip4: yes
do-ip6: no # Set to yes if you need IPv6 resolution and forwarding
do-udp: yes
do-tcp: yes

## Access control: allow queries from localhost
# Allow queries from localhost AND the Docker bridge network
access-control: 127.0.0.0/8 allow
access-control: 172.17.0.0/16 allow # Adjust subnet mask if needed
# access-control: ::1/128 allow # If using IPv6

# Hide identity and version
hide-identity: yes
hide-version: yes

# Define host.docker.internal to point to Windows Host's LAN IP
# Local data for host.docker.internal
# Format: local-data: "hostname. A IN IP_Address"
# Note: Unbound is more strict; ensure FQDN-like names or use local-zone.
# For simple A record:
local-data: "host.docker.internal. A 192.168.1.109" # YOUR_WINDOWS_LAN_IP
local-data-ptr: "192.168.1.109 host.docker.internal." # For reverse lookup - YOUR_WINDOWS_LAN_IP



# Forwarding mode (instead of unbound acting as a full recursive resolver)
forward-zone:
name: "." # Forward all queries
# Forward to your router or public DNS
forward-addr: 8.8.8.8
forward-addr: 8.8.4.4
#forward-addr: 1.1.1.1 # Cloudflare
#forward-addr: 192.168.1.1 # YOUR_WINDOWS_DEFAULT_GATEWAY

3. Check, Start, and Enable Unbound:

sudo unbound-checkconf
sudo systemctl start unbound
sudo systemctl enable unbound
sudo systemctl status unbound # Verify active (running)

4. Verify Unbound Listening Ports:

sudo ss -tulnp | grep unbound
# Should show unbound listening on BOTH 127.0.0.1:53 and DOCKER_BRIDGE_IP:53

Configure WSL2 Host DNS Resolution

This tells the WSL2 host itself to use the local Unbound server.

  1. Configure /etc/resolv.conf:

If file exists run first

sudo chattr -i /etc/resolv.conf
sudo rm -f /etc/resolv.conf

Now, run:

sudo nano /etc/resolv.conf

and put ONLY

nameserver 127.0.0.1

Finally, run

sudo chattr +i /etc/resolv.conf

This makes file immutable (as it originally was), preventing it changes on WSL startup.

2. Make Persistent:

Edit /etc/wsl.conf in WSL2. Under [network] add generateResolvConf = false.

This is how the file I have looks.

[boot]
systemd=true

[user]
default=aberkovich

[network]
generateResolvConf = false

A wsl --shutdown and restart is needed for this /etc/wsl.conf change to fully apply regarding /etc/resolv.conf management.

Disable resolvconf Update Scripts (Recommended)

Packages like unbound or dnsmasq might install scripts in /etc/resolvconf/update.d/ that attempt to dynamically manage /etc/resolv.conf via the resolvconf framework. Since we are managing /etc/resolv.conf manually and disabled WSL’s automatic generation, these scripts are unnecessary and could potentially interfere.

  1. Check for Scripts:
ls -l /etc/resolvconf/update.d/

2. Disable/Remove Them:

Move any scripts related to DNS services (like unbound or dnsmasq) out of this directory to prevent them from running.

# Example: Moving the unbound script
sudo mkdir -p /etc/resolvconf/update.d.disabled
sudo mv /etc/resolvconf/update.d/unbound /etc/resolvconf/update.d.disabled/
# Repeat for dnsmasq or others if present

Configure Docker Daemon DNS via daemon.json

This explicitly tells Docker which DNS server its containers should use, ensuring they target the Unbound server listening on the docker bridge IP.

  1. Configure /etc/docker/daemon.json:

Edit or create the Docker daemon config file in WSL2:

sudo nano /etc/docker/daemon.json

Add the following, using the DOCKER_BRIDGE_IP you found earlier (e.g., 172.17.0.1):

{
"dns": ["172.17.0.1"]
}

2. Restart Docker Daemon:

sudo systemctl restart docker
sudo systemctl status docker # Verify active (running)

Test Container DNS Usage and Connectivity

  1. Run a Test Container:
docker run - rm -it alpine sh

2. Check Docker Container’s /etc/resolv.conf:

Inside the container’s shell:

cat /etc/resolv.conf
# Expected: nameserver 172.17.0.1 (nameserver DOCKER_BRIDGE_IP)

3. Test DNS Resolution Inside Container:

# Install DNS/Network utilities
apk update && apk add bind-tools netcat-openbsd
# Test host.docker.internal resolution
nslookup host.docker.internal
# Expected: Resolves to YOUR_WINDOWS_LAN_IP (e.g., 192.168.1.109) via DOCKER_BRIDGE_IP
# Test external resolution
nslookup google.com
# Expected: Resolves successfully

4. Test Connection to Host Service (e.g., SSH Tunnel on Port 3333):

Start SSH Tunnel on Windows. See https://alex-ber.medium.com/ssh-tunnel-in-windows-and-wsl2-950ea7adfd92 for more details.

# Inside container
nc -v -z -w 3 host.docker.internal 3333
# Expected: Connection to host.docker.internal port 3333 [tcp/*] succeeded!

Why This Final Setup Works

  • WSL2 Networking: NAT mode avoids mirrored mode’s DNS interference.
  • Unbound Configuration: Unbound listens on both localhost (for WSL2 host) and the Docker bridge IP (for docker containers). Crucially, it resolves host.docker.internal to the host’s reliable LAN IP, bypassing potential WSL2 NAT issues with the virtual host IP.
  • Docker Daemon Configuration: Explicitly setting the DNS in daemon.json to the Docker bridge IP forces containers to query Unbound directly on that interface.
  • Container Connection: Containers resolve host.docker.internal to the host LAN IP. WSL2’s NAT routes this connection correctly to the host machine where services (like an SSH tunnel listening on 0.0.0.0) can accept it.

Potential Considerations

IP Address Changes

The host LAN IP, WSL virtual IP, and Docker bridge IP can change. This configuration is more resilient as the host LAN IP is often more stable, but monitor if issues arise.

Firewall on Windows

Ensure the Windows firewall allows connections from the WSL2 virtual IP range and the Docker container IP range (172.17.0.0/16) to necessary host ports. Even though we connect to the LAN IP, the source will be from WSL2/Docker IPs.

Conclusion

Resolving host.docker.internal reliably for Docker containers running directly in WSL2 requires careful attention to WSL2s networking mode, local DNS server configuration (including which IP to resolve to), and Dockers DNS settings. By using WSL2s NAT mode, configuring Unbound to resolve host.docker.internal to the hosts LAN IP and listen on the Docker bridge, and explicitly setting Dockers DNS via daemon.json, you can achieve seamless host-container communication without relying on --add-host.

Appendix A

Here is my .wslconfig file. You can find him on Windows on
C:\Users\<YourUsername>\.wslconfig

[wsl2]
# Limits VM memory to use no more than 4 GB, this can be set as whole numbers using GB or MB
memory=16GB #9GB #4 GB
# Sets the VM to use two virtual processors
processors=4
# Sets the swap size to 8 GB
swap=8GB #1GB #2 GB
# Enables mirrored networking mode instead of the default NAT mode
#networkingMode=mirrored

[experimental]
# Automatically releases cached memory after detecting idle CPU usage. Set to gradual for slow release, and dropcache for instant release of cached memory.
autoMemoryReclaim=dropcache
  • memory=16GB: Setting a memory limit prevents WSL 2 from consuming excessive system RAM, which is useful on systems with limited memory or when running multiple resource-intensive applications.. You can specify the value in gigabytes (GB) or megabytes (MB) as whole numbers.
  • processors=4: This specifies the number of virtual CPU cores (processors) allocated to the WSL 2 VM. Limiting the number of processors can help balance performance between WSL 2 and other applications on your system. For example, on a system with 8 cores, setting processors=4 ensures WSL 2 uses only half of the available cores.
  • swap=8GB: This sets the size of the swap file (virtual memory) for the WSL 2 VM to 8 GB. A larger swap size allows more memory-intensive tasks to run without crashing, but it relies on disk storage, which is slower than RAM. Setting an appropriate swap size balances performance and stability.
  • When no networkingMode is specified in .wslconfig, WSL 2 defaults to NAT mode. In NAT mode, the WSL 2 VM operates behind a virtual network interface, and network traffic is routed through the Windows host.

Note: networkingMode=mirrored enables “mirrored” networking mode for WSL 2 instead of the default NAT (Network Address Translation) mode. Mirrored mode allows the WSL 2 VM to share the host’s network interfaces directly. As we saw, this doesn’t work well.

  • autoMemoryReclaim=dropcache: This setting enables automatic release of cached memory when WSL 2 detects idle CPU usage. The value dropcache means cached memory is released instantly when idle. WSL 2 can cache memory to improve performance, but this may consume RAM unnecessarily when the system is idle. Enabling autoMemoryReclaim helps free up memory for other applications on the host system. gradual as another option, which would release cached memory more slowly over time.

Appendix B

Here is my /etc/wsl.conf file. This file is on WSL2 itself.

[boot]
systemd=true
command="bash /home/aberkovich/starup.sh"

[user]
default=aberkovich

[network]
generateResolvConf = false

Here is /home/aberkovich/starup.sh file:

#!/bin/bash
echo "WSL started" >> /home/aberkovich/wsl-log.txt
sudo systemctl daemon-reload
sudo systemctl restart unbound

You should run chmod +x startup.sh in order him to work.

--

--

alex_ber
alex_ber

Written by alex_ber

Senior Software Engineer at Pursway

No responses yet