Why It Happens
UFW's rules sit on the path for traffic addressed to the server itself. Docker handles a published port earlier: a rule in the nat table rewrites the destination to the container's address, so the packet is now traffic passing through the server to another network, and it follows Docker's forwarding rules instead. UFW never sees it. You can see Docker's rules and what is published:
docker ps --format '{{.Names}}\t{{.Ports}}' # 0.0.0.0:8080->80/tcp = open on every address
sudo iptables -t nat -L DOCKER -n # the redirect rules Docker addedAlways confirm from another machine, since a test from the server itself proves nothing: nc -zv SERVER_IP 8080 or curl -I http://SERVER_IP:8080.
Fix 1: Publish on 127.0.0.1 and Proxy
The cleanest fix for web apps. Publish the port on the loopback address only, so it is reachable from the server but not from outside, and let Nginx or another reverse proxy on the server handle HTTPS on 443:
# docker run
docker run -d -p 127.0.0.1:8080:80 nginx
# docker compose
services:
app:
image: nginx
ports:
- "127.0.0.1:8080:80"Containers that only talk to each other (a database for your app, for example) need no ports: entry at all: on the same Compose network they reach each other by service name. Running Docker on a VPS sets this up from the start.
Fix 2: Filter in DOCKER-USER
Docker processes the DOCKER-USER chain before its own rules, and leaves it alone, so rules there apply to published ports. To allow outside access to containers only from one address, find the public interface with ip route get 1.1.1.1 (the name after dev), then insert two rules. The second is inserted above the first and keeps replies to the containers' own outgoing connections working:
sudo iptables -I DOCKER-USER -i eth0 ! -s 203.0.113.5 -j DROP sudo iptables -I DOCKER-USER -i eth0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT sudo iptables -L DOCKER-USER -n --line-numbers
Replace eth0 and the address with your own. These rules last until reboot; save them with your distribution's method (for example the iptables-persistent package on Debian and Ubuntu). This applies to Docker's default iptables mode.
Fix 3: Block It Outside the Server
A firewall in front of the server is not affected by anything Docker does inside it. On Virteche Cloud, the firewall on the server's page in the dashboard is enforced on the host: allow only the ports you mean to expose (for example 22, 80 and 443) and a container published on 8080 stays unreachable from the internet. Use it alongside Fix 1 rather than instead of it, so a mistake in one layer is caught by the other.
For how UFW, firewalld and nftables relate to each other, see UFW vs firewalld vs iptables vs nftables.
Docker is a trademark of its owner. Virteche is not affiliated with or endorsed by it. Details about the product come from its public documentation as of the date at the top of this guide.