Skip to main content

Docker Bypasses UFW: How to Firewall Container Ports

Security · 31.10.2026 · 6 min read
Illustration for “Docker Bypasses UFW: How to Firewall Container Ports”

An administrator enables UFW, denies all incoming connections, and the container port is still reachable from the internet. This is not a UFW misconfiguration: Docker manages iptables on its own, and traffic to published ports never reaches the rules that UFW writes. Below you will find how to see the problem and how to close the ports properly.

Why UFW does not protect a published port

UFW adds its rules to the INPUT chain, which handles packets addressed to the server itself. When you publish a port with the -p option, Docker creates a DNAT rule in the nat table and forwards packets to the container. This traffic does not go to the server itself but passes through the FORWARD chain, where Docker rules come first. The UFW rules from INPUT do not apply to it.

ChainWritten byWhat it handles
INPUTUFWTraffic to the server itself
DOCKER-USERAdministratorForwarded traffic to containers, checked first
DOCKERDockerPermissions for published ports

How to check that the port is visible from outside

Start a test container with a published port and look at the PORTS column: the entry 0.0.0.0:8080 means the port listens on all interfaces. Then check availability from another computer. The address in the example comes from the documentation range, so use the address of your own server.

docker run -d --name web -p 8080:80 nginx
docker ps
ufw status
nc -zv 203.0.113.10 8080

If the connection succeeds while ufw status shows a deny policy, you are seeing exactly this behavior. You can inspect the Docker rules like this:

iptables -L DOCKER-USER -n -v --line-numbers
iptables -t nat -L DOCKER -n
iptables -L FORWARD -n -v
ss -tlnp

Option one: publish only on 127.0.0.1

The simplest and most reliable approach is not to expose the port to the external network when it is needed only locally, for example by a reverse proxy on the same host. Put the bind address before the port. External access to the port then becomes impossible, while Nginx on the host still reaches the container through 127.0.0.1.

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

services:
  web:
    image: nginx
    ports:
      - "127.0.0.1:8080:80"

The default bind address for all containers can also be set with the ip parameter in /etc/docker/daemon.json, followed by a Docker restart. The iptables parameter set to false turns off rule management, but it breaks outgoing network access for containers, so it is not recommended as a permanent solution.

Option two: rules in DOCKER-USER

If the port must be reachable only from certain addresses, add rules to the DOCKER-USER chain: Docker does not overwrite it and checks it before its own rules. By the time of the check the packet has already passed DNAT, so the destination port has changed. To filter by the original port, use the conntrack module with the --ctorigdstport parameter.

iptables -I DOCKER-USER -i eth0 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL -j DROP
iptables -I DOCKER-USER -i eth0 -s 198.51.100.7 -p tcp -m conntrack --ctorigdstport 8080 --ctdir ORIGINAL -j ACCEPT
iptables -L DOCKER-USER -n -v --line-numbers

The ACCEPT rule was inserted after DROP, so it ends up higher and matches first. These rules do not survive a reboot on their own: save them, for example with the iptables-persistent package, and check the result after restarting Docker and the server. For more on chains, see iptables: setting up a Linux firewall from scratch.

Option three: isolated Docker networks

Containers that need no external access do not need published ports. Databases and caches only have to join a shared user-defined network: containers reach each other by name, and from outside they are invisible. A network created with the --internal option additionally has no route to the internet.

docker network create --internal backend
docker run -d --name db --network backend mariadb

In Docker Compose the same is done with the networks block and the internal: true parameter; details are in Docker Compose: running an application stack. Basic firewall setup is described in UFW Firewall on a VPS, and Docker installation in Docker on a VPS: installing and running containers.

Frequently asked questions

Why does Docker open ports bypassing UFW?

Docker adds its own iptables rules: DNAT in the nat table and permissions in the FORWARD chain. Traffic to a container is forwarded rather than sent to the INPUT chain, where UFW works. That is why a UFW deny rule does not affect a published port, and you need DOCKER-USER rules or a 127.0.0.1 binding.

Is publishing on 127.0.0.1 enough?

If the application is needed only by processes on the same server, for example Nginx acting as a reverse proxy, binding to 127.0.0.1 is enough: the port does not listen on external interfaces. If selected external addresses need access, use DOCKER-USER rules or a closed network such as a VPN.

Do I need to publish ports so containers can talk to each other?

No. Containers in the same user-defined Docker network reach each other by service name and internal port, so publishing is not required. A port is published only for access from outside. Keep databases and queues in a network without publishing, and they stay unreachable from the internet.

Will DOCKER-USER rules survive a reboot?

Not by themselves: iptables rules live in kernel memory. Save them using the standard method of your distribution, for example the iptables-persistent package, and after a reboot run iptables -L DOCKER-USER to confirm that the rules are in place and that starting Docker did not disturb their order.

Was this article helpful?
← Back to Knowledge Base Ask Support