docker run -p 5432:5432 punches through UFW by writing its own iptables rules. Your database may be publicly reachable even though UFW 'denies' it.
The DOCKER chain processes published ports before UFW's INPUT rules ever see the packet.
docker run -p 127.0.0.1:5432:5432 postgres # or omit -p and use a network
iptables -I DOCKER-USER -p tcp --dport 5432 ! -s 10.0.0.0/8 -j DROP
ss -tlnp | awk '$4 !~ /127.0.0.1|\[::1\]/'
Assume every -p is public until proven otherwise. Cloud firewalls (security groups, Cloudflare Access) are the reliable boundary — host firewalls do not reliably constrain Docker.
Docker writes its own iptables NAT rules (DOCKER chain) ahead of UFW's chains: published ports are exposed before UFW INPUT rules evaluate. It's a rule-ordering reality, not a UFW bug — UFW's deny does not apply to Docker's forwarded traffic.
Bind them to the intended interface: -p 127.0.0.1:5432:5432 for local-only, or the LAN IP for internal services. Optionally use DOCKER-USER chain rules for explicit allowlists — the bind approach is simpler and self-documenting.
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.