UFW is iptables underneath. When rules seem to do nothing, the cause is one of: not enabled, wrong direction, Docker bypassing it, or a more-specific rule earlier in the chain winning.
ufw status must say active. 'ufw allow out' vs 'in' matter; default deny incoming must be set. The single most common miss: adding rules while inactive.
Published container ports (-p 8080:80) punch through UFW via the DOCKER chain — iptables -L DOCKER -n shows it. UFW rules never see that traffic.
iptables processes in order: if 'allow from 10.0.0.5' precedes 'deny 8080', that host still gets through — and looks like a broken deny.
sudo ufw status verbose # must say Status: active; check Default deny (incoming)
sudo iptables -S | grep -E '8080|DOCKER' | head -10 && sudo iptables -L INPUT -n --line-numbers | head -15
# never publish to all interfaces: -p 127.0.0.1:8080:80 # or DOCKER-USER chain rules for external traffic shaping
sudo ufw status numbered && sudo ufw delete <n> # allows before denies = deny never fires; re-add deny first
Cloud security groups (AWS/Azure/DO firewalls) act BEFORE UFW — a blocked port there looks exactly like an unexplained firewall failure in-guest. Test from a truly external host: nc -vz <ip> <port> — testing from the server itself exercises loopback, not the firewall.
Docker writes its own nat/DOCKER chains that jump before the UFW chains for container traffic. UFW simply never sees those packets. Publish to 127.0.0.1, use DOCKER-USER, or run a host-level proxy instead.
sudo iptables -L -v -n adds packet/byte counters per rule — hit the port from outside and watch the counter climb. No movement = traffic isn't reaching that rule.
Our most-documented failures, packaged as ready-to-ship starter kits: Docker, Kubernetes, and Terraform.
Browse the template store →One-time. Yours to modify. Instant download from the NinjaOps template store.