Skip to content
Products
AI Website Builder New VPS Hosting Cloud Servers Web Hosting Dedicated Servers Domains
Company
About Documentation Support Center Contact Get Started Call +91 75795 45488
Login
Hosting Panel — cPanel & Billing Console Panel — VPS Management
ALL SYSTEMS OPERATIONAL
VPS

Docker Bypassing Your UFW Firewall? Here's the Real Fix

Getwebup 7 min read

You ran ufw deny 3306, checked ufw status, saw it listed as denied, and moved on. Then a security scan — or a curious visitor with nmap — shows that port wide open to the internet anyway. If you're running Docker on the same box, this isn't a fluke. It's how Docker and UFW are built to interact, and it catches almost everyone who runs both.

Symptom: UFW says denied, the port answers anyway

The usual way people find this out is the hard way. You publish a container with something like:

docker run -d -p 3306:3306 --name mysql-db mysql:8

Then you lock it down with UFW because you only want your app server hitting that port:

sudo ufw deny 3306
sudo ufw status

UFW happily reports the rule as active. But run this from an outside machine and it still connects:

nc -zv your-server-ip 3306
# Connection to your-server-ip 3306 port [tcp/mysql] succeeded!

Same story with any published port — Postgres on 5432, Redis on 6379, a raw app container on 8080. UFW's rule exists, UFW isn't lying to you, and the port is still reachable from anywhere on the internet.

Cause: Docker writes its own iptables rules, and they run first

UFW is a friendly wrapper around iptables (or nftables on newer distros via the legacy-compat layer). When you publish a container port, Docker doesn't ask UFW's permission — it inserts its own rules directly into the kernel's netfilter tables, in the DOCKER, DOCKER-USER, and FORWARD chains.

Here's the part that trips people up: traffic to a published container port is forwarded traffic, not traffic destined for the host itself. It goes through the NAT table's PREROUTING and DOCKER chains, then the FORWARD chain — never through INPUT. UFW's rules (the ones you add with ufw allow/ufw deny) primarily govern the INPUT chain, because that's where traffic destined for the host's own services lands. Docker's rules in the DOCKER chain get evaluated first for forwarded traffic and explicitly ACCEPT it to reach the container — before UFW's chain is ever consulted.

You can see this for yourself:

sudo iptables -L DOCKER -n --line-numbers
sudo iptables -L FORWARD -n --line-numbers

You'll find Docker's own ACCEPT rules for your published ports sitting there, completely independent of anything UFW wrote. This is documented, intentional behavior on Docker's part — not a bug — but it's rarely obvious until you've been burned by it.

Fix 1: Use the DOCKER-USER chain (the officially supported way)

Docker reserves the DOCKER-USER chain specifically for admin-added rules, and guarantees it's evaluated before Docker's own forwarding rules — and it survives container restarts and docker daemon reloads (a plain reboot still needs persistence, covered below). This is the right place to restrict access to published ports.

To allow only your office or app-server IP to reach port 3306, and drop everyone else:

sudo iptables -I DOCKER-USER -i eth0 -p tcp --dport 3306 ! -s 203.0.113.10 -j DROP
sudo iptables -I DOCKER-USER -j RETURN

Replace eth0 with your actual public interface (check with ip a) and 203.0.113.10 with the IP that should be allowed through. The -I flag inserts the rule at the top of the chain, ahead of Docker's own entries — order matters here, since iptables evaluates top to bottom and stops at the first match.

To block a port entirely from the outside world (say, a database that only internal containers should reach over the Docker network):

sudo iptables -I DOCKER-USER -i eth0 -p tcp --dport 3306 -j DROP

Rules added this way aren't persisted across a reboot by default. Install iptables-persistent so they survive:

sudo apt install iptables-persistent
sudo netfilter-persistent save

Fix 2: Don't publish the port to the world in the first place

Often the real fix isn't a firewall rule at all — it's not exposing the container port publicly to begin with. If only your own app on the same host needs to reach the database, bind it to localhost instead of all interfaces:

docker run -d -p 127.0.0.1:3306:3306 --name mysql-db mysql:8

With that, Docker's NAT rule only forwards traffic arriving on the loopback interface — nothing external can reach it, UFW or no UFW. This is the cleanest option for databases, caches, and internal APIs that never need to be internet-facing.

For services that genuinely need to be public (a web app, an API), put them behind a reverse proxy — Nginx, Caddy, or Traefik — bound to 80/443, and keep the container itself on localhost or an internal Docker network. Traffic to 80/443 on the host itself goes through the INPUT chain, where UFW's rules work exactly as expected.

Fix 3: Use host networking when isolation isn't the point

If a container is using network_mode: host instead of the default bridge network, there's no NAT and no DOCKER chain involved — the process binds directly to the host's network stack, and UFW's normal INPUT rules apply to it like any other service. This trades away Docker's network isolation, so it's not right for every container, but for a single-purpose VPS running one app, it removes the whole problem.

Fix 4: Tell Docker to stop managing iptables (advanced, has tradeoffs)

You can disable Docker's iptables management entirely by adding this to /etc/docker/daemon.json:

{
  "iptables": false
}

Then restart Docker: sudo systemctl restart docker. Now UFW is the only thing writing firewall rules, and its rules apply as you'd expect.

The catch: Docker also uses iptables to set up NAT for outbound container traffic and inter-container communication. Turn it off and container-to-container networking and internet access from inside containers can break unless you replicate those NAT rules yourself. This option is worth it if you fully understand the tradeoff — most people are better served by Fix 1 or Fix 2.

Which fix fits your setup

SituationRecommended fix
Port only needs to be reachable from your own server or appBind to 127.0.0.1, skip publishing it externally
Port must be reachable from specific external IPs (office, partner API)DOCKER-USER chain with a source IP allowlist
Public web app on 80/443 via reverse proxyBind proxy to host, keep app container internal — normal UFW rules apply
Single-purpose VPS, no need for container network isolationnetwork_mode: host
You want UFW to be the single source of truth and understand the NAT tradeoff"iptables": false in daemon.json

Verifying the fix actually worked

Don't trust ufw status alone for anything Docker-related — check from outside the server:

# from a different machine
nmap -p 3306,5432,6379 your-server-ip

# on the server, confirm DOCKER-USER is doing the blocking
sudo iptables -L DOCKER-USER -n -v --line-numbers

If nmap reports the port as filtered or closed from an unauthorized IP, and open only from the allowed one, the rule is working. Re-run this check after any host reboot to confirm your persistence step actually held.

Prevention: bake this into how you deploy containers

  • Default to 127.0.0.1:PORT:PORT for anything that isn't meant to be public — make binding to 0.0.0.0 a deliberate choice, not the default.
  • Keep a running list of every port a -p flag or docker-compose.yml publishes on the box, and check it against iptables -L DOCKER -n periodically — compose files drift, and a port added six months ago is easy to forget.
  • If you manage several containers, write your DOCKER-USER rules into a small shell script under version control, and load it from a systemd unit that runs after docker.service — that survives reinstalls and new servers, not just reboots.
  • Run an external port scan against your server's public IP after every deploy that touches container networking, not just after firewall changes.

The underlying lesson: on a box running Docker, UFW secures the host's own services, not automatically anything a container publishes. Once that split is clear, the rest is just remembering which chain a given port's traffic actually flows through.

Frequently asked questions

Does this affect every Docker network mode, or just bridge?

It's specific to the default bridge network and any published ports (the -p flag or ports: in compose). Containers using network_mode: host bind straight to the host's network stack with no NAT involved, so UFW's normal INPUT chain rules apply to them directly — this issue doesn't come up.

Will docker-compose down and back up remove my DOCKER-USER rules?

No. DOCKER-USER is reserved for admin rules and Docker doesn't touch it when containers start, stop, or restart. It does get flushed if the Docker daemon itself is reinstalled or the iptables rules were never persisted past a reboot, so pair this with iptables-persistent or a systemd unit that reapplies your rules on boot.

I'm on a distro using nftables by default (Debian 12, recent Ubuntu). Does the DOCKER-USER fix still work?

Yes, as long as Docker is using the iptables-nft compatibility binary, which is the default on these distros — it translates the same iptables commands into nftables rules under the hood. Run sudo iptables -V to confirm you're on the nf_tables backend; the commands in this guide work unchanged either way.

Is it safer to just close the port at the cloud provider's firewall (e.g., a security group) instead?

That works too and is arguably simpler if your provider offers it — a cloud-level firewall sits in front of the server entirely, so it doesn't care what Docker does locally. Many teams use both: the provider firewall as the primary gate, and DOCKER-USER rules as a second layer in case a security group ever gets misconfigured.

How do I check what ports Docker has actually opened on this server right now?

Run docker ps to see published ports per container, then cross-check with sudo iptables -L DOCKER -n -v to see the actual forwarding rules Docker installed. If a port shows up in one but you can't explain it from the other, treat that as worth investigating before you assume it's safe.

#docker #ufw #iptables #firewall #vps-security

Keep reading

Chat with Support