How to Make UFW and Docker Work Together (Stop Containers Bypassing Your Firewall)

Docker bypasses your firewall. Fix it. ufw-docker: containers finally obey UFW.

Tutorial 6 warned about it and the reverse-proxy tutorials worked around it: a published Docker port is reachable from the network even when UFW says it's blocked. Docker writes its own iptables rules ahead of UFW's. For a server behind a home router that's a nuisance; for a cloud server it's a hole. ufw-docker is a small script that makes Docker's traffic go through UFW like everything else, after which ufw allow and ufw deny mean what they say for containers too.

Quick reference

TaskWhat
See the problemsudo ufw deny 8096 then open http://server-ip:8096 — it still loads
Install ufw-dockerdownload the script to /usr/local/bin/ufw-docker, chmod +x
Apply the fixsudo ufw-docker install then sudo systemctl restart ufw
Allow a containersudo ufw-docker allow jellyfin 8096
From your LAN onlysudo ufw route allow proto tcp from 192.168.1.0/24 to any port 8096
List / removesudo ufw-docker list jellyfin · sudo ufw-docker delete allow jellyfin 8096

1. See it for yourself

sudo ufw status
sudo ufw deny 8096/tcp

Now open Jellyfin at http://192.168.1.50:8096 from your laptop. It loads. The deny rule did nothing, because Docker inserted its own ACCEPT in the DOCKER chain, which is evaluated before UFW's chains. Remove the test rule (sudo ufw delete deny 8096/tcp).

2. Install ufw-docker

sudo wget -O /usr/local/bin/ufw-docker https://github.com/chaifeng/ufw-docker/raw/master/ufw-docker
sudo chmod +x /usr/local/bin/ufw-docker
sudo ufw-docker install
sudo systemctl restart ufw

install appends a block to /etc/ufw/after.rules that routes Docker's traffic through a ufw-user-forward chain and drops anything not explicitly allowed. Nothing else about UFW changes.

Test again: http://192.168.1.50:8096 now doesn't load. Containers are closed by default — the way the rest of the server already was.

3. Allow what you mean to

sudo ufw-docker allow jellyfin 8096
sudo ufw-docker allow uptime-kuma 3001
sudo ufw-docker list jellyfin

Each rule is tied to a container name (it resolves the container's current IP and adds a route rule). For things that should only be reachable from the house:

sudo ufw route allow proto tcp from 192.168.1.0/24 to any port 9443

4. The reverse-proxy pattern, now airtight

If every app sits behind Nginx or Caddy (tutorials 28, 57), allow only the proxy:

sudo ufw-docker allow caddy 80
sudo ufw-docker allow caddy 443

Apps keep ports: published for convenience, or drop them entirely and use a shared Docker network (tutorial 101) — either way, nothing but 80 and 443 is reachable from outside. Combined with UFW's own rules for SSH and WireGuard, sudo ufw status is finally the complete truth about what's open.

5. Keep it working

Alternative: no published ports at all

If you'd rather not run extra rules, the cleanest approach is 127.0.0.1: binds for everything plus a proxy (tutorial 28), and no ports: on apps that only the proxy needs to reach. ufw-docker is for the cases where a port must exist and must be governed.

Troubleshooting

Nothing works after ufw-docker install. Expected until you allow things — including the proxy. Allow 80/443 for the proxy container first.

ufw-docker allow says "ERROR: The container doesn't exist". The name must match docker ps exactly; use container_name: in compose.

Rules vanish after a reboot. The after.rules block is there (grep ufw-docker /etc/ufw/after.rules) but per-container allows were tied to old IPs. Re-run the allows; consider fixed IPs.

Containers can't reach the internet. DEFAULT_FORWARD_POLICY in /etc/default/ufw must be ACCEPT (Docker's NAT needs it); ufw-docker's rules still filter inbound.

Wrapping up

Prove the hole with one deny rule, close it with ufw-docker install, open only the proxy. From then on the firewall you've been maintaining since tutorial 6 actually governs the containers — and ufw status tells you everything that's reachable, with no exceptions you forgot about.

Call us