How Docker Breaks UFW and Alters iptables

Let me set the context for this article: I was deploying some applications on my VPS, and while I had configured UFW (Uncomplicated Firewall), the ports published by Docker were still exposed directly to the internet.
In this article, we’ll explore this issue from the basics all the way to a robust, production-ready solution.
What is UFW?
UFW, or Uncomplicated Firewall, is a tool used to protect your systems. To put it simply, every computer has around 65,536 ports through which it can be accessed over a network, and most applications typically use a single port.
UFW acts as a simple interface for managing the system’s IP tables.
Here’s a diagram to help visualize how it works:

For example, the standard port for PostgreSQL is 5432, so the database normally runs on this port.
UFW is then used to allow or block traffic to specific ports as needed.
Problem statement
Assume you block all ports on your system and then run a Docker image that starts a container with a port mapping, for example 8000:8000. This means the container’s internal port 8000 is mapped to the host’s port 8000.
Now, even if you have enabled the firewall and explicitly denied access to port 8000, someone from the internet can still reach it.
Sounds surprising? It definitely is, and it’s a serious security concern.
Let’s take an example of this issue
Enabling UFW

When enabling UFW on a VPS, make sure to allow port 22 for SSH; otherwise, you will be locked out.

After that, you can allow or deny ports according to your needs. You can view the current UFW rules using the status command.

Let’s try denying a port. Logically, we want no one to access the service running on a specific port—in this case, port 5555.

To test this, we can use the
Test-NetConnectioncommand from a local machine to check if the port is open.Here, you can see that the TCP test is successful.

This means that even after explicitly blocking port 5555 with UFW, it is still accessible.
Something doesn’t add up, right?
Root cause behind this?
Docker’s default behavior is port-first, security-second.
Docker is not aware of “public vs private” networks, it just exposes sockets on the host.
This is why it seems illogical: a port you think should be private by default is actually exposed to the world unless you explicitly configure the firewall.
Let’s solve this
Modify the UFW configuration file /etc/ufw/after.rules and add the following rules at the end of the file:
# BEGIN UFW AND DOCKER
*filter
:ufw-user-forward - [0:0]
:ufw-docker-logging-deny - [0:0]
:DOCKER-USER - [0:0]
-A DOCKER-USER -j ufw-user-forward
-A DOCKER-USER -m conntrack --ctstate RELATED,ESTABLISHED -j RETURN
-A DOCKER-USER -m conntrack --ctstate INVALID -j DROP
-A DOCKER-USER -i docker0 -o docker0 -j ACCEPT
-A DOCKER-USER -j RETURN -s 10.0.0.0/8
-A DOCKER-USER -j RETURN -s 172.16.0.0/12
-A DOCKER-USER -j RETURN -s 192.168.0.0/16
-A DOCKER-USER -j ufw-docker-logging-deny -m conntrack --ctstate NEW -d 10.0.0.0/8
-A DOCKER-USER -j ufw-docker-logging-deny -m conntrack --ctstate NEW -d 172.16.0.0/12
-A DOCKER-USER -j ufw-docker-logging-deny -m conntrack --ctstate NEW -d 192.168.0.0/16
-A DOCKER-USER -j RETURN
-A ufw-docker-logging-deny -m limit --limit 3/min --limit-burst 10 -j LOG --log-prefix "[UFW DOCKER BLOCK] "
-A ufw-docker-logging-deny -j DROP
COMMIT
# END UFW AND DOCKER
sudo systemctl restart ufw or sudo ufw reload required to restart UFW after changning the files.
If after this UFW rules didn’t take effect, reboot your VPS.
What does this solution actually do?
One sentence:
It makes Docker-published ports private by default and only public when UFW explicitly allows them.
Docker can still publish ports normally (
-p 8080:80works).Containers and private/internal networks can access those ports without restriction.
Public/internet traffic is blocked by default from reaching Docker-published ports.
UFW controls access:
ufw allow 8080opens it to the public, removing the rule closes it.Docker’s networking is untouched: no disabling iptables, no hard-coded IPs.
Let’s check

and, the connection is successfully rejected.
Opening a docker-published port to the world
To allow a Docker-published port through UFW, you can use:
sudo ufw route allow proto tcp to any port 5555
This command allows external traffic to access all published ports where the container port is 5555.
Note: If you publish a port using -p 12080:5555, you must reference the container port (5555), not the host port (12080).
With this configuration, Docker-published ports now respect UFW rules, effectively solving the core issue.
If you want to know more about this, head over to this amazing repository: https://github.com/chaifeng/ufw-docker
Connect with me
Email: m.safi.ullah@outlook.com
GitHub: github.com/safi-io
If you learned something from this article, subscribe to the newsletter and share your questions in the comments below.