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

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
| Task | What |
|---|---|
| See the problem | sudo ufw deny 8096 then open http://server-ip:8096 — it still loads |
| Install ufw-docker | download the script to /usr/local/bin/ufw-docker, chmod +x |
| Apply the fix | sudo ufw-docker install then sudo systemctl restart ufw |
| Allow a container | sudo ufw-docker allow jellyfin 8096 |
| From your LAN only | sudo ufw route allow proto tcp from 192.168.1.0/24 to any port 8096 |
| List / remove | sudo 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
- Container IPs change on recreate; ufw-docker rules use the IP at the time. Run sudo ufw-docker allow name port again after docker compose up -d recreates a container, or give long-lived containers a fixed IP on a user network. A tiny cron line that re-applies your allows nightly is a pragmatic fix.
- After a Docker upgrade, check sudo ufw-docker check — it tells you if after.rules needs re-installing.
- docker.sock-based tools (Portainer, Watchtower) are unaffected.
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.
