· via dev.to (home feed)
Docker's published ports bypass UFW, leaving denied services open to the internet
A dev.to post shows how Docker's iptables rules let traffic slip past UFW's deny rules, leaving ports like Postgres and Redis exposed, and outlines four configuration fixes.

A developer writing on dev.to found that three server ports they had explicitly blocked in UFW — PostgreSQL on 5432, Redis on 6379 and an internal admin dashboard on 8081 — were fully reachable from the internet. A connection test from a laptop on a coffee shop network hit the PostgreSQL port on the first attempt, and a port scan confirmed all three denied ports were open. The cause was not a bug or a misconfigured rule, but a structural gap between where Docker and UFW each hook into the Linux firewall.
How the bypass works
When a container port is published, either with docker run -p 5432:5432 or an equivalent Compose ports entry, Docker binds it on every interface and installs its own iptables rules. According to the dev.to post, an incoming packet first hits the nat table's PREROUTING chain, where Docker has inserted a jump to its own DOCKER chain. A DNAT rule there rewrites the packet's destination to the container's internal IP address.
The kernel then treats the packet as traffic to be forwarded to a container rather than traffic delivered to the host itself. Forwarded packets pass through the filter table's FORWARD chain, where Docker has added its own accept rules for published ports. UFW's deny rules, however, sit on the INPUT chain, which the packet never enters. UFW only polices traffic addressed to the host, so its status output keeps reporting the port as denied even though it is open to the outside world.
How to test for it
The author stresses that checks from localhost prove nothing, because loopback traffic follows a different path through the firewall. The reliable test is a scan from a different machine, for example nmap against the server's public address. On the host itself, ss -tlnp shows docker-proxy listening on 0.0.0.0 for the published port, and iptables -t nat -L DOCKER reveals the DNAT rule — both signs that the port sidesteps UFW.
The post also describes how easily this happens in practice: Compose port mappings added so a GUI database client could connect during development were later copied to a production VPS and never removed. The Redis instance ran without a password on the assumption it was confined to the Docker network, and the PostgreSQL logs showed failed login attempts from unknown IP addresses, consistent with bots scanning for default credentials. The author saw no evidence of a breach but attributes that to luck rather than engineering.
The recommended fixes
The post ranks four fixes. First, do not publish ports for internal services at all — containers on the same Compose network can reach each other by service name, so an application can connect to postgres:5432 without any ports section. Second, when host access is genuinely needed, bind to loopback with a mapping such as 127.0.0.1:5432:5432 and tunnel in over SSH. The DNAT rule still exists in that setup, but it only matches traffic addressed to localhost, so nothing arriving on the public interface gets through.
Third, make loopback the default by setting "ip": "127.0.0.1" in /etc/docker/daemon.json. A bare -p flag then binds to 127.0.0.1 instead of 0.0.0.0, and exposing something publicly requires an explicit 0.0.0.0 prefix. Existing containers must be recreated after the change, since they keep the bindings they were created with. Fourth, for ports that must be public but restricted, filter traffic in the DOCKER-USER chain, which Docker jumps to at the top of FORWARD and leaves untouched. Because DNAT has already rewritten the destination by that point, rules need to match the original port using conntrack. The author warns against disabling Docker's iptables management with "iptables": false, which trades one surprise for several quieter forms of breakage in container networking.
Why it matters
UFW is one of the most common perimeter tools on Linux servers, and in this scenario its status output is actively misleading: it reports denied ports as blocked while the internet can still reach them. Any team running Docker on a firewalled host — particularly with databases, caches or admin tooling published for convenience — may be exposing services they believe are closed. The failure is silent, the audit is cheap, since a scan from any outside machine will reveal it, and the fixes are configuration-level changes. That combination makes this a worthwhile default check for anyone shipping Compose files from a laptop to a server.
- #docker
- #ufw
- #security
- #firewall
- #linux
- #devops