🛠️ ITBlogs.pro

Docker and UFW: Why Published Ports Bypass Your Firewall

Oct 16, 2026 · Docker · 3 min read

This surprises almost everyone: you run ufw deny 8080 or ufw enable, yet a container published with -p 8080:80 is still reachable from the internet. UFW didn't fail. Docker simply doesn't go through the chain UFW controls.

Why it happens

UFW filters traffic in iptables' INPUT chain, which is for packets addressed to the host itself. When you publish a port, Docker adds a NAT rule that redirects incoming packets to the container, so they are forwarded, not delivered locally. They travel through the FORWARD chain and Docker's own DOCKER chain, and UFW's rules never see them.

Check what you are really exposing:

docker ps --format 'table {{.Names}}\t{{.Ports}}'
sudo ss -tlnp | grep docker-proxy

Ports shown as 0.0.0.0:8080->80/tcp or :::8080->80/tcp are open to everyone, whatever ufw status says. Test from another machine to be sure:

nc -zv SERVER_IP 8080

Fix 1 (best): bind to localhost

Publish only on the loopback interface, and let a reverse proxy (Apache, Nginx, Caddy) handle public traffic:

docker run -d -p 127.0.0.1:8080:80 nginx

In Compose:

ports:
  - "127.0.0.1:8080:80"

Now only the host can reach the container; the proxy on ports 80/443 (which UFW does control) is the only way in. This is the simplest and safest rule: never publish a port you don't want the world to see. Databases, Redis and admin panels should never be published publicly at all, and services on the same Docker network can talk without published ports.

Fix 2: filter with the DOCKER-USER chain

Docker leaves the DOCKER-USER chain alone, and it's checked before Docker's own rules, so it's the supported place for your own restrictions.

Block outside access to a published container port (here the container is published on 8080):

sudo iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL -j DROP

Allow only one trusted address first (insert it above the DROP rule):

sudo iptables -I DOCKER-USER -i eth0 -s 203.0.113.10 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL -j ACCEPT

Notes:

  • Replace eth0 with your public interface (ip -br addr).
  • --ctorigdstport is needed because the destination port has already been rewritten to the container port by the time the rule runs.
  • These rules do not survive a reboot by themselves. Persist them with iptables-persistent, or put them in /etc/ufw/after.rules.
  • Repeat with ip6tables if your containers are reachable over IPv6.

Fix 3: a helper tool

ufw-docker installs the needed rules once and then lets you manage container ports with commands like ufw route allow. It's convenient but you should read what it changes to your firewall files first.

What not to do

Setting "iptables": false in /etc/docker/daemon.json stops Docker from touching the firewall, but it also breaks container NAT and networking, so you would have to maintain all the rules yourself. It is not a beginner fix.

A second layer

Your hosting provider's cloud firewall or security group filters traffic before it reaches the server at all. Allow only the ports you intend (22, 80, 443) there as well. If Docker bypasses UFW by mistake, the provider firewall still blocks the port.

Checklist

  1. List published ports with docker ps.
  2. Anything that shouldn't be public gets 127.0.0.1: in front of the port mapping.
  3. Verify from an outside machine with nc -zv or an online port scanner.
  4. Add a provider-level firewall as a backstop.
  5. Re-check after adding any new container.