Troubleshooting: Linux UFW Firewall Blocking Docker Container Port Exposures on Ubuntu 20.04 LTS
Resolve UFW blocking Docker container port access on Ubuntu 20.04 LTS. Understand iptables conflicts and restore container connectivity.
Resolve UFW blocking Docker container port access on Ubuntu 20.04 LTS. Understand iptables conflicts and restore container connectivity.
When deploying Docker containers on an Ubuntu 20.04 LTS server, you might encounter a frustrating scenario: despite correctly exposing container ports using docker run -p or docker-compose's ports directive, and even seeing the container running and listening internally, external access to these services fails. This typically manifests as connection timeouts or refused connections from clients trying to reach your containerized application. The culprit, more often than not, is an intricate interplay between UFW (Uncomplicated Firewall) and Docker's own iptables management. This guide will walk you through the technical details and provide robust solutions.
Symptom & Error Signature
The primary symptom is the inability to access services running inside a Docker container from an external network or even from the host machine's public IP address, while local access (e.g., curl localhost:PORT or curl 172.17.0.x:PORT from within the host) might work perfectly.
Typical Observations:
From an external client:
$ curl http://YOUR_SERVER_IP:8080 curl: (7) Failed to connect to YOUR_SERVER_IP port 8080: Connection refusedor
$ telnet YOUR_SERVER_IP 8080 Trying YOUR_SERVER_IP... telnet: connect to address YOUR_SERVER_IP: Connection timed outOn the Docker host:
docker psconfirms the container is running and ports are mapped:$ sudo docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES abcdef123456 nginx "/docker-entrypoint.…" 2 minutes ago Up 2 minutes 0.0.0.0:8080->80/tcp, :::8080->80/tcp webserver- Local access works:
$ curl http://localhost:8080 <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> ... - UFW status might even show the port allowed (which is misleading for Docker's forwarding):
$ sudo ufw status verbose Status: active Logging: on (low) Default: deny (incoming), allow (outgoing), deny (routed) New profiles: skip To Action From -- ------ ---- 8080/tcp ALLOW IN Anywhere
iptablesoutput might not immediately reveal the issue to the untrained eye, but the order of rules is critical.
Root Cause Analysis
The core of the problem lies in the interaction and rule order precedence between UFW and Docker's iptables management.
Docker's
iptablesManagement: When Docker exposes a port (e.g.,-p 8080:80), it directly manipulates theiptablesrules. It adds rules primarily to theNATtable (for port forwarding) and theFILTERtable (specifically in theFORWARDchain and its customDOCKERandDOCKER-USERchains) to allow traffic to and from the container. These rules are designed to punch holes in the firewall for container traffic.UFW's
iptablesManagement: UFW, as a user-friendly frontend foriptables, also manages rules in theFILTERtable. Crucially, UFW by default sets theDEFAULT_FORWARD_POLICY="DROP"in/etc/default/ufw. This means any traffic that is forwarded (i.e., traffic that isn't destined for the host itself but is passing through the host to another network, like a Docker container) will be dropped unless explicitly allowed by an earlier rule.The Conflict (Rule Order): When UFW is enabled or reloaded, it inserts its rules relatively early in the
iptableschains, specifically within thefiltertable. Docker's rules, while present, often appear after UFW's defaultDROPrules for theFORWARDchain. This creates a "race condition" or, more accurately, a precedence issue:- An incoming connection for
YOUR_SERVER_IP:8080hits thePREROUTINGchain in theNATtable, where Docker'sDOCKERchain performs the destination NAT (DNAT) to172.17.0.x:80. - The packet then moves to the
FORWARDchain in theFILTERtable. - Because UFW's rules are typically positioned earlier and often include a
DEFAULT_FORWARD_POLICY="DROP", the packet gets dropped by UFW's rules before it reaches Docker's rules that would explicitlyACCEPTthis forwarded traffic to the container. - Even if you use
sudo ufw allow 8080/tcp, this rule typically applies to theINPUTchain (for traffic to the host itself), not theFORWARDchain (for traffic through the host to a container).
- An incoming connection for
In essence, UFW's default behavior effectively nullifies Docker's iptables forwarding rules by dropping the forwarded packets first.
Step-by-Step Resolution
The most robust and recommended solution involves modifying UFW's before.rules file to explicitly allow forwarded traffic that Docker manages, before UFW's default DROP rules take effect.
1. Backup UFW Configuration Files
Always back up critical configuration files before making changes. This allows for easy rollback if anything goes wrong.
sudo cp /etc/ufw/before.rules /etc/ufw/before.rules.bak
sudo cp /etc/ufw/before6.rules /etc/ufw/before6.rules.bak # For IPv6 if in use
2. Identify Docker Bridge Network
You need to know the IP range of your Docker bridge network (usually docker0). This is crucial for crafting specific iptables rules.
ip a show docker0
Example Output:
3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:0a:92:49:15 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:aff:fe92:4915/64 scope link
valid_lft forever preferred_lft forever
From this output, we see the Docker network is 172.17.0.0/16. You will use 172.17.0.0/16 for IPv4 rules.
3. Modify UFW's before.rules for Docker Forwarding
Edit /etc/ufw/before.rules using your preferred text editor (e.g., nano or vim).
sudo nano /etc/ufw/before.rules
Scroll down to the section that defines rules for the FORWARD chain. Look for comments like # Don't delete these required lines, otherwise there will be no TCP/IP forwarding or similar.
The exact placement within
before.rulesis important. You want these rules to be evaluated before anyDROPrules that might block Docker's forwarded traffic. A good place is often after the*filtertable declaration and before the standard UFW rules begin.
Add the following rules. Replace 172.17.0.0/16 with your Docker bridge network range if it's different.
# START DOCKER CUSTOM RULES
# Allow all traffic from Docker containers to the outside world
-A ufw-before-forward -i docker0 -j ACCEPT
# Allow all traffic from the outside world to Docker containers through the Docker bridge (if forwarded by Docker's NAT)
-A ufw-before-forward -o docker0 -j ACCEPT
# Allow traffic from specific external interface to Docker bridge (e.g., eth0 to docker0)
# This rule is crucial for incoming connections
-A ufw-before-forward -i <YOUR_MAIN_NETWORK_INTERFACE> -o docker0 -j ACCEPT
# ACCEPT all connections coming from the Docker network and going out
# This ensures containers can talk to the internet
-A FORWARD -s 172.17.0.0/16 -j ACCEPT
# ACCEPT all connections going into the Docker network from the outside
# This ensures external access to exposed container ports
-A FORWARD -d 172.17.0.0/16 -j ACCEPT
# If you specifically want to control the DOCKER-USER chain, you can ensure it's jumped to early:
# -A FORWARD -j DOCKER-USER
# END DOCKER CUSTOM RULES
Replace
<YOUR_MAIN_NETWORK_INTERFACE>with your actual main network interface (e.g.,eth0,ens3,enp0s3). You can find this usingip a. Example:eth0.The rules
-A FORWARD -s 172.17.0.0/16 -j ACCEPTand-A FORWARD -d 172.17.0.0/16 -j ACCEPTare broad. They allow all forwarded traffic to/from your Docker network. While generally necessary for Docker to function correctly with UFW, ensure your Docker containers are secure. Docker's internalDOCKERchain will still apply specific port filtering.
Revised and more targeted approach within before.rules (recommended):
Find the section that starts with *filter and contains rules for the FORWARD chain. Add these rules above the line -A ufw-before-forward -j ufw-user-forward and also above any general DROP rules for the FORWARD chain.
#
# rules.before
#
# Rules that should be run before the ufw command line added rules.
#
*filter
:ufw-before-input - [0:0]
:ufw-before-output - [0:0]
:ufw-before-forward - [0:0]
:ufw-not-local - [0:0]
# Don't delete these required lines, otherwise there will be no TCP/IP forwarding
-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -i docker0 -j ACCEPT # Docker to outside
-A FORWARD -o docker0 -j ACCEPT # Outside to Docker
# Insert these BEFORE the "default deny" rules for the FORWARD chain
# See https://docs.docker.com/network/iptables/#forwarding-to-containers-from-the-outside-world
# Allowing established and related connections
-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
# Explicitly allow traffic from the Docker bridge to anywhere
# This allows containers to reach the internet or other hosts
-A FORWARD -i docker0 -j ACCEPT
# Explicitly allow traffic from anywhere to the Docker bridge
# This allows external access to services exposed by Docker (Docker will handle specific ports via DNAT)
-A FORWARD -o docker0 -j ACCEPT
# Ensure DOCKER-USER chain is traversed for more granular control if you use it
-A FORWARD -j DOCKER-USER
# Continue with UFW's standard rules
# -A ufw-before-forward -j ufw-not-local
...
For IPv6, perform the same modification to /etc/ufw/before6.rules, adapting the IP range (e.g., fe80::/64 or your Docker IPv6 subnet if configured).
sudo nano /etc/ufw/before6.rules
Add similar rules using ip6tables. For example:
# Rules for IPv6
*filter
# ... existing ufw-before-forward rules ...
# DOCKER IPv6 FORWARD RULES
-A FORWARD -i docker0 -j ACCEPT
-A FORWARD -o docker0 -j ACCEPT
-A FORWARD -j DOCKER-USER
# END DOCKER IPv6 FORWARD RULES
# ... rest of the ufw-before-forward rules ...
The exact
docker0IPv6 range depends on Docker's IPv6 network configuration. If you haven't explicitly enabled IPv6 for Docker, you might not need the IPv6 rules here, but it's good practice if you plan to.
4. Configure UFW to Allow the Specific Port (Optional, but Good Practice)
While the before.rules modification handles the forwarding aspect, it's still good practice to explicitly allow the port in UFW for clarity and for any non-Docker services that might use it. This rule applies to the INPUT chain, allowing traffic to the host itself, which then gets forwarded by Docker's NAT.
sudo ufw allow 8080/tcp comment 'Allow Nginx container on port 8080'
# If you need UDP:
# sudo ufw allow 5000/udp comment 'Allow application UDP on port 5000'
5. Reload UFW and Restart Docker
To apply the before.rules changes, you must reload UFW. The most reliable way to do this is to disable and then enable it. It's also a good idea to restart Docker to ensure its iptables rules are re-applied correctly after UFW.
sudo ufw disable
sudo ufw enable
sudo systemctl restart docker
Disabling UFW temporarily exposes your server to the internet without a firewall. Do this only if you understand the risks and for a brief period. If you're on a production server, consider doing this during a maintenance window or ensuring other network-level firewalls are in place.
6. Verification
After performing the steps, verify that your container is now accessible.
Check UFW status:
sudo ufw status verboseEnsure it's active and your new port rules (if any in step 4) are listed.
Check
iptablesrules (optional, for advanced users):sudo iptables -nvL FORWARDYou should see your
ufw-before-forwardrules (and potentially theDOCKERrules) appearing in theFORWARDchain, withACCEPTactions for traffic to/from the Docker bridge network. The order is key: yourACCEPTrules should precede any generalDROPrules.Test external access:
curl http://YOUR_SERVER_IP:8080This should now return the expected output from your containerized application.
7. Alternative 1: Modify UFW's Default Forward Policy (Less Secure)
Another approach, often found online, is to change UFW's default forward policy.
sudo nano /etc/default/ufw
Change DEFAULT_FORWARD_POLICY="DROP" to DEFAULT_FORWARD_POLICY="ACCEPT".
# DEFAULT_FORWARD_POLICY="DROP"
DEFAULT_FORWARD_POLICY="ACCEPT"
Then reload UFW:
sudo ufw disable
sudo ufw enable
This method is generally NOT recommended for production environments. Setting
DEFAULT_FORWARD_POLICY="ACCEPT"significantly weakens your firewall's security posture by allowing all forwarded traffic by default, unless explicitly blocked by other rules. This could expose other internal networks or services if your server has multiple interfaces or complex routing. Thebefore.rulesmodification is more targeted and secure.
8. Alternative 2: Use Docker's DOCKER-USER chain (Advanced, for Fine-Grained Control)
Docker provides a special DOCKER-USER chain for users to add their own rules, which are applied before Docker's automatic rules in the FORWARD chain. This allows you to explicitly permit or deny traffic to/from containers based on source IP, interface, etc., without altering UFW's core configuration files.
Rules added to
DOCKER-USERare volatile. They will be reset ifiptablesis flushed or Docker is restarted withoutiptables-persistentor similar. To make them persistent, you'll need to save them.
Add
iptablesrules directly (temporary):# Allow traffic from eth0 destined for port 80 (exposed by container) to be forwarded to docker0 sudo iptables -I DOCKER-USER -i eth0 -o docker0 -p tcp --dport 80 -j ACCEPT # Allow traffic from eth0 destined for port 443 (exposed by container) to be forwarded to docker0 sudo iptables -I DOCKER-USER -i eth0 -o docker0 -p tcp --dport 443 -j ACCEPTReplace
eth0with your main network interface and80/443with your exposed container ports.Make
iptablesrules persistent: Installiptables-persistent:sudo apt install iptables-persistentSave your current
iptablesrules:sudo netfilter-persistent saveThis will save the rules to
/etc/iptables/rules.v4and/etc/iptables/rules.v6, ensuring they are reloaded on boot.
This approach provides very granular control but requires managing iptables directly and ensuring persistence. It's often used when specific source IPs or interfaces need different access policies to containers. For most standard deployments, modifying before.rules is simpler and equally effective.
Our Production Verification Guarantee
Encountering a bug not covered here or running a non-standard kernel configuration? Our solutions are continually refined against real production incidents. Submit an environment trace for our editorial team to replicate.