Making host.docker.internal
Work in WSL2
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:
- 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. - 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.
- 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.
- 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.
- Modify WSL2 Network Configuration (Use NAT Mode):
Disable the problematicnetworkingMode=mirrored
.
- Edit
.wslconfig
:
OpenC:\Users\<YourUser>\.wslconfig
. - Remove/Comment Out Mirrored Mode:
EnsurenetworkingMode=mirrored
is removed or commented out under[wsl2]
.
[wsl2]
# networkingMode=mirrored
- Restart WSL2:
Exit from all WSL2 bash/terminals.
From cmd/Power Shell runwsl --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)
anddisabled
. - 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 resolvehost.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).
- 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.
- 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.
- 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.
- 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
- 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 resolveshost.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 on0.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. EnablingautoMemoryReclaim
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.