I've seen this exact situation play out dozens of times. Someone sets up a Docker server, installs UFW, adds a deny rule for their database port, checks ufw status, sees "Status: active" and the deny rule listed, and assumes they're protected.
Then they run a port scan from an external machine and their Redis instance is wide open.
This isn't a bug. It's how Docker works. But it trips up almost everyone the first time — and sometimes the second time too.
What UFW actually does (and doesn't do)
UFW manages Linux's iptables rules. Specifically, it manages the INPUT chain — the chain that handles traffic coming into the host machine itself.
Here's the problem: Docker doesn't use the INPUT chain for container traffic. It uses the FORWARD chain and a custom DOCKER chain it creates itself. Traffic destined for a container gets routed through FORWARD before it ever reaches INPUT.
So when you run ufw deny 6379, you're blocking direct connections to port 6379 on the host. But when Docker maps a container's Redis port to 0.0.0.0:6379, that traffic flows through the DOCKER chain — and UFW never sees it.
The result: Your UFW rules look correct. Your Redis port is open to the internet anyway.
How to actually see the problem
Run this:
sudo iptables -L DOCKER --line-numbers
You'll see ACCEPT rules that Docker added. Every port-mapped container gets one. These rules run before your UFW INPUT rules ever fire.
Now run:
sudo iptables -L FORWARD | grep DOCKER
Docker inserts itself into the FORWARD chain too. It's thorough about this.
The fixes — pick one
Option 1: Bind to 127.0.0.1 (cleanest, recommended)
Instead of letting Docker bind to all interfaces, tell it to bind only to localhost. Traffic then only reaches the container via your reverse proxy, not directly from the internet.
In docker-compose.yml:
# Before — exposed to internet: ports: - "6379:6379" # After — localhost only: ports: - "127.0.0.1:6379:6379"
This works because Docker's iptables rules only forward traffic from the bound interface. If it's bound to 127.0.0.1, external traffic never triggers the forwarding rule.
Option 2: DOCKER-USER chain rules
Docker reserves a chain called DOCKER-USER specifically for user-defined rules that run before Docker's own rules. Rules you add here actually stick.
sudo iptables -I DOCKER-USER -p tcp --dport 6379 -j DROP sudo iptables -I DOCKER-USER -p tcp --dport 6379 -s 127.0.0.1 -j ACCEPT
The catch: these rules don't survive a reboot unless you persist them with iptables-persistent.
sudo apt install iptables-persistent sudo netfilter-persistent save
Option 3: Internal Docker networks (for container-to-container traffic)
If you just need containers to talk to each other without exposing ports to the host, don't publish ports at all. Use Docker's internal networking:
services:
app:
networks:
- internal
redis:
networks:
- internal
# No ports: block at all
networks:
internal:
internal: true
With internal: true, the network has no external connectivity. Containers can reach each other by service name, but nothing outside the Docker network can reach them.
How to verify you're actually protected
Don't trust ufw status alone. Run a real test from outside:
# From another machine (replace with your server IP): nc -zv YOUR_SERVER_IP 6379 # Or use nmap: nmap -p 6379 YOUR_SERVER_IP
If the port is closed, you're done. If it's open, you have the bypass problem.
Quick check: Paste your ufw status verbose output into the ConfigClarity Firewall Auditor. It flags Docker bypass risk and shows which ports are potentially exposed despite your UFW rules.
Why this happens on fresh installs
The default Docker install modifies iptables without asking. The default UFW install doesn't know about Docker. Neither tool warns you about the conflict. So you end up with two systems that both appear to be working correctly but are silently fighting each other.
The fix is a one-line change to your docker-compose.yml. Do it for every service that doesn't need to be publicly accessible — databases, caches, internal APIs, anything behind a reverse proxy.
Paste your ufw status verbose output and get an instant audit of your firewall rules — Docker bypass detection included.