Resolving UFW Blocking Docker Container Port Exposures on WSL2 Ubuntu

Troubleshoot UFW blocking Docker container ports on WSL2 Ubuntu. Learn why Docker's iptables rules conflict and how to properly configure UFW for seamless exposure.


Troubleshoot UFW blocking Docker container ports on WSL2 Ubuntu. Learn why Docker's iptables rules conflict and how to properly configure UFW for seamless exposure.

This guide addresses a common yet often perplexing issue for developers and system administrators utilizing Docker on Windows Subsystem for Linux 2 (WSL2): the Linux Uncomplicated Firewall (UFW) preventing access to ports exposed by Docker containers. Despite correctly mapping container ports (e.g., docker run -p 80:80 nginx), attempts to access the service from the Windows host or other network devices fail, leading to connection timeouts or refusals. This guide will walk you through the root cause and a robust, secure resolution.

Symptom & Error Signature

Users typically experience one or more of the following symptoms:

  • No connectivity from Windows host:

    # From Windows PowerShell
    Invoke-WebRequest -Uri http://localhost:80 -UseBasicParsing -TimeoutSec 5 # Or your exposed port
    # Expected output: Request will hang and eventually time out or fail with "The remote name could not be resolved" or "A connection attempt failed because the connected party did not properly respond after a period of time"
    
    # From Windows PowerShell
    Test-NetConnection -ComputerName localhost -Port 80 # Or your exposed port
    # Expected output:
    # ComputerName     : localhost
    # RemoteAddress    : ::1
    # RemotePort       : 80
    # TcpTestSucceeded : False  (or true if the service is running but only locally, not accessible from host)
    # The `TcpTestSucceeded : False` is the key indicator.
    
  • Docker container appears to be running correctly inside WSL2:

    # Inside WSL2 Ubuntu terminal
    docker ps
    # CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS          PORTS                                   NAMES
    # a1b2c3d4e5f6   nginx     "/docker-entrypoint.…"   2 minutes ago    Up 2 minutes    0.0.0.0:80->80/tcp, :::80->80/tcp       webserver
    
    # Inside WSL2 Ubuntu terminal, verify the port is listening
    sudo ss -tuln | grep -E ":80|:443" # Or your specific port
    # Expected output (showing Docker-proxy listening):
    # tcp   LISTEN  0       4096   0.0.0.0:80      0.0.0.0:*
    # tcp   LISTEN  0       4096      [::]:80         [::]:*
    
  • UFW is active, potentially with a default deny policy:

    # Inside WSL2 Ubuntu terminal
    sudo ufw status verbose
    # Expected output often includes:
    # Status: active
    # Logging: on (low)
    # Default: deny (incoming), allow (outgoing), deny (routed) # <- The "deny (routed)" is often the culprit
    # New connections: skip
    

Root Cause Analysis

The core of this problem lies in the interaction and precedence of iptables rules managed by Docker and UFW (Uncomplicated Firewall) within your WSL2 Ubuntu environment.

  1. WSL2 as a Virtual Machine: Remember that WSL2 runs a real Linux kernel in a lightweight virtual machine. It has its own network stack and firewall, separate from the Windows host.
  2. Docker's iptables Management: When you expose a port using Docker (e.g., -p 80:80), Docker automatically modifies the host's iptables rules. It typically adds rules to the NAT table for port forwarding and to the FILTER table (specifically the DOCKER chain and FORWARD chain) to allow traffic to and from containers.
  3. UFW's iptables Management: UFW is a user-friendly frontend for iptables. When UFW is enabled, it sets up its own comprehensive set of iptables rules, often with a default policy to deny incoming and deny forwarded (routed) traffic.
  4. The Conflict: The critical conflict arises in the FORWARD chain of iptables. Docker inserts rules into this chain to allow traffic to reach your containers. However, if UFW is active and its DEFAULT_FORWARD_POLICY is set to DROP or DENY (which is common and secure by default), UFW's rules will often take precedence or simply drop the traffic before Docker's specific forwarding rules are ever evaluated. This means the traffic trying to reach your Docker container from the Windows host (which is treated as "forwarded" traffic by UFW from the WSL2 perspective) is blocked.
  5. Lack of Specific UFW Rules: Even if UFW has general "allow" rules, it might not explicitly know how to handle the dynamic iptables rules created by Docker for port forwarding unless specifically configured to do so.

Step-by-Step Resolution

The most robust solution involves two parts: configuring UFW to acknowledge Docker's networking, and then explicitly allowing the necessary ports through UFW.

Modifying UFW configuration files directly can lead to loss of network connectivity if done incorrectly. Always back up the files before making changes. sudo cp /etc/ufw/before.rules /etc/ufw/before.rules.bak sudo cp /etc/default/ufw /etc/default/ufw.bak

1. Verify UFW Status and Docker Network

Before making changes, confirm the current state.

# Inside WSL2 Ubuntu terminal

# Check UFW status (should be active with 'deny (routed)' or similar)
sudo ufw status verbose

# List Docker containers and their exposed ports
docker ps

# Inspect Docker's iptables rules for the FORWARD chain
# Look for rules referencing the DOCKER chain or specific container IPs
sudo iptables -L FORWARD -n -v | grep -i docker

# Identify your Docker bridge network interface and its subnet (usually docker0)
ip a | grep docker0
# Example output:
# 3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
#     inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
# Note the IP range (e.g., 172.17.0.0/16). This will be used in the next step.

2. Configure UFW's before.rules for Docker Networking

This step ensures UFW allows traffic to traverse the Docker bridge network, letting Docker's own iptables rules manage internal container routing. We'll modify /etc/ufw/before.rules to insert rules that prioritize Docker's forwarding.

# Inside WSL2 Ubuntu terminal
sudo nano /etc/ufw/before.rules

Scroll to the end of the file. You will add two distinct blocks: one for the filter table and one for the nat table. Place these blocks after any existing UFW-generated rules, typically at the very end of the file, ensuring they are before the primary COMMIT for the filter table (if any) or as new table definitions.

# START DOCKER UFW FILTER RULES (for forwarding)
# These rules allow traffic to and from the docker0 bridge interface
# in the 'filter' table, preventing UFW from blocking Docker's forwarding.
# Place these lines within the *filter table section, ideally at the end
# of the :ufw-before-forward chain rules, but before the main filter COMMIT.

-A ufw-before-forward -i docker0 -j ACCEPT
-A ufw-before-forward -o docker0 -j ACCEPT
-A ufw-before-forward -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

# END DOCKER UFW FILTER RULES

# START DOCKER UFW NAT RULES (for masquerading)
# This *nat table block must be self-contained with its own COMMIT.
# It ensures Docker containers can access external networks by masquerading their traffic.
# Adjust the subnet (e.g., 172.17.0.0/16) if your docker0 interface uses a different range.
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
COMMIT
# END DOCKER UFW NAT RULES

The *nat table block, including its own COMMIT, must be a distinct section. If your before.rules already has a *nat section, integrate the -A POSTROUTING rule there. Otherwise, place the entire *nat block at the very end of the file. Ensure you do not add a COMMIT line inside the *filter block if one already exists for that table. UFW processes these sections carefully during reload.

3. Allow Specific Exposed Ports through UFW

While the before.rules modifications help Docker's internal routing, you still need to explicitly tell UFW to permit traffic to the specific ports your Docker containers expose on the WSL2 host. These rules are added to UFW's managed chains.

# Inside WSL2 Ubuntu terminal

# Example: If your Docker container exposes port 80 (e.g., -p 80:80)
sudo ufw allow 80/tcp comment 'Allow HTTP traffic to Docker containers'

# Example: If your Docker container exposes port 443 (e.g., -p 443:443)
sudo ufw allow 443/tcp comment 'Allow HTTPS traffic to Docker containers'

# Example: If your Docker container exposes port 8080 (e.g., -p 8080:8080)
sudo ufw allow 8080/tcp comment 'Allow custom service traffic to Docker containers'

# Add `ufw allow` commands for all TCP/UDP ports you expose via Docker.
# For UDP ports, use `sudo ufw allow <port>/udp`.

These ufw allow commands create rules that permit incoming traffic on the specified ports, which UFW processes correctly, including for forwarded traffic to Docker containers.

4. Reload UFW and Restart Docker

Apply the UFW changes and restart Docker to ensure all iptables rules are re-evaluated and applied correctly.

# Inside WSL2 Ubuntu terminal

# Disable and re-enable UFW to apply `before.rules` changes
sudo ufw disable
sudo ufw enable

# Verify UFW status again
sudo ufw status verbose
# You should now see your allowed ports listed in the status output.

# Restart the Docker daemon
sudo systemctl restart docker
# If systemctl isn't fully working in your WSL2 setup, try:
# sudo service docker restart

5. Verify Resolution

Now, attempt to access your Docker service from the Windows host.

# From Windows PowerShell

# Test connectivity (replace 80 with your exposed port)
Test-NetConnection -ComputerName localhost -Port 80

# If successful, TcpTestSucceeded should be True
# You can also try to access it via your browser: http://localhost:80

You should now be able to access your Docker containers' exposed ports from your Windows host.

WSL2 IP Changes: While you can usually access services via localhost from Windows, remember that the WSL2 VM's IP address can change each time it starts. If you need to access it from other machines on your network, you might need to find the current WSL2 IP (ip a | grep eth0) and configure your Windows firewall to forward that specific IP, or ensure your Windows firewall allows traffic to the WSL2 network adapter.

Persistent UFW Rules: The ufw allow rules are persistent across reboots. The changes to /etc/ufw/before.rules are also persistent. This setup should survive WSL2 restarts.