Why Docker Can Bypass UFW (And How to Fix It)
Docker can expose ports even when UFW is configured to block them. Here's why it happens and how to fix it safely.
- Jacob
- 4 min read

Introduction
Recently, I configured a VPS to host my web app - The Scoreboard App. It’s backed by a PostgreSQL database and Redis for caching, both running inside Docker. These services only need to be accessed by my back-end service, which runs on the same machine, so they don’t need any public access. I decided on using UFW (Uncomplicated Firewall) to lock down the server. By default, UFW denies all incoming traffic, and you add rules to open specific ports. For example, you can allow incoming HTTPS traffic like so:
sudo ufw allow 443 # HTTPS traffic uses port 443
Perfect, the server should now block traffic to Postgres and Redis, right?
That’s what I thought. Unfortunately, this was not the case. A quick port scan using Nmap (a network mapper/port scanner) revealed that ports 5432 and 6379 were open to the public!
PORT STATE SERVICE
22/tcp open ssh
443/tcp open https
5432/tcp open postgresql
6379/tcp open redis
So my first thought was I must’ve configured UFW incorrectly. Running sudo ufw status verbose revealed:
# ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), deny (routed)
New profiles: skip
To Action From
-- ------ ----
22/tcp ALLOW IN Anywhere
443/tcp ALLOW IN Anywhere
Nope, that’s all set up correctly - the default incoming rule is deny, and only ports 22 and 443 have been explicitly allowed.
So what’s going on?
By looking at the Docker docs we can see that Docker configures firewall rules with iptables for containers with exposed ports:
| Docker creates iptables rules in the host’s network namespace for bridge networks
UFW under the hood configures iptables. However, Docker inserts its own iptables rules that are evaluated before UFW’s filtering rules. So despite UFW reporting that the ports were closed, Docker had effectively exposed them. This is intended behaviour - UFW isn’t broken, Docker’s rules just take precedence. The reason for this is that UFW primarily manages the filter table, while Docker modifies both the nat and filter tables - adding rules that can accept and forward traffic before UFW’s rules are applied.
Let’s have a closer look at the docker-compose.yml configuration that caused this:
redis:
image: redis:7-alpine
restart: unless-stopped
ports:
# This line bypasses UFW
- 6379:6379
In this configuration, we’ve said map port 6379 on the host to port 6379 in the container. Because we didn’t specify a host interface, Docker binds the port to all available interfaces (0.0.0.0), exposing it to the entire internet.
The Solution
I fixed this in two ways. The first was to update my docker-compose.yml to only listen on localhost (127.0.0.1):
redis:
image: redis:7-alpine
restart: unless-stopped
ports:
# Now this port is only opened to connections on the same host
- 127.0.0.1:6379:6379
This is the recommended solution, and usually does the trick. However, to avoid similar issues in the future, I decided to also configure iptables to prevent Docker from exposing ports to the public. This was done by adding the following to /etc/ufw/after.rules:
# Ensure this is under `*filter`
# Define the DOCKER-USER chain
:DOCKER-USER - [0:0]
# Block external traffic. NOTE: `eth0` may be different on your machine
-A DOCKER-USER -i eth0 -j DROP
# Allow everything else (internal/local)
-A DOCKER-USER -j RETURN
Save the file and reload UFW to apply the changes with:
sudo ufw reload
This is a bit of a nuclear option - it blocks all external traffic routed to Docker containers. If you have public-facing services running inside Docker (like an Nginx reverse proxy or your actual web app), this will break them. This fix is only suitable if Docker is strictly for internal backend services.
Now Docker won’t expose anything to the public, so UFW has full control over what traffic is allowed in. A quick Nmap scan confirms the fix:
PORT STATE SERVICE
22/tcp open ssh
443/tcp open https
5432/tcp filtered postgresql
6379/tcp filtered redis
As you can see, Redis and Postgres are now blocked to the public.
Ensuring the Ports Stay Closed
To stay on top of this, I use Uptime Kuma to monitor my services. I’ve configured a monitor for both Postgres and Redis - these attempt to connect to the TCP port of each and notify me if the port is open (using the “Upside Down Mode”). If I ever accidentally expose the ports in the future, I’ll get a notification immediately, allowing me to fix it before it becomes a problem.
Conclusion
If you’re ever running Docker containers, please remember that Docker may expose ports in a way that bypasses UFW rules unless configured carefully. To avoid surprises, always be explicit about your port bindings in your docker-compose.yml.
But my biggest takeaway? Never blindly trust your configuration. Always verify your firewall rules with a quick Nmap scan.
Stay secure, and happy coding!