You set up UFW, allowed only SSH, 80 and 443, and ran ufw status to check. Then you started a container with -p 8080:80, and port 8080 is open to the whole internet anyway.
This isn't a bug in your UFW rules. It's how Docker networking works on iptables-based hosts, which is still the default. Here's why it happens, how to check your own server, and three ways to fix it.
Why UFW doesn't see container ports
UFW mostly manages the INPUT chain: traffic addressed to the host itself.
When you publish a port, Docker adds a NAT rule that rewrites incoming traffic for host:8080 to the container's IP and port. After that rewrite, the packet isn't for the host any more. It's forwarded to the container, so it goes through the FORWARD chain, where Docker's own rules accept it. UFW's INPUT rules never see it.
So ufw deny 8080 does nothing for a published container port.
Check your server in 30 seconds
List what your containers publish:
docker ps
In the PORTS column, anything shown as 0.0.0.0:8080->80/tcp (or [::]:8080->...) is reachable from outside, whatever UFW says. 127.0.0.1:8080->80/tcp is local only.
Then test from another machine, not from the server itself:
nc -zv your.server.ip 8080
If it says succeeded or open, the port is public.
Fix 1: bind to localhost (best for most setups)
If the service sits behind a reverse proxy on the same server (Caddy, Nginx, Traefik), it doesn't need a public port at all:
services:
app:
image: ghcr.io/you/app:1.4.2
ports:
- "127.0.0.1:8080:80" # only the reverse proxy can reach it
Or with docker run:
docker run -d -p 127.0.0.1:8080:80 ghcr.io/you/app:1.4.2
Then point the proxy at 127.0.0.1:8080. This is the simplest fix and needs no firewall changes.
Containers in the same Compose project can already reach each other by service name over the project network. A database container should normally have no ports: entry at all.
Fix 2: filter in the DOCKER-USER chain
When a port really must be public, but only to some addresses, put the rule in DOCKER-USER. Docker creates that chain for you and checks it before its own forwarding rules, and doesn't overwrite it.
One catch: by the time traffic reaches DOCKER-USER, it has already been rewritten to the container's port. To match the host port you published, match the original destination port with conntrack:
# Allow 8080 only from your office range, drop it for everyone else
sudo iptables -I DOCKER-USER -i eth0 -p tcp \
-m conntrack --ctorigdstport 8080 --ctdir ORIGINAL \
! -s 203.0.113.0/24 -j DROP
Replace eth0 with your public interface (ip route get 1.1.1.1 shows it) and the address range with yours.
Rules added this way don't survive a reboot. Save them with iptables-persistent (sudo netfilter-persistent save), or add them to UFW's /etc/ufw/after.rules in a *filter block for DOCKER-USER. If your containers use IPv6, add the same rule with ip6tables.
Don't "fix" this by setting "iptables": false in Docker's daemon.json. That stops Docker from managing its own rules, and container networking breaks in ways that are hard to debug.
Fix 3: a firewall in front of the server
A network firewall at your hosting provider filters traffic before it reaches the server, so Docker's rules don't matter. Allow 22, 80 and 443 there and everything else is closed, including container ports you forgot about.
This is the safest backstop, and it works well alongside fix 1. Most VPS providers offer one for free. On VeloxaHost it's under Firewalls → New Firewall, attached per server.
Which one to use
-
Web apps behind a reverse proxy: fix 1. Bind everything except the proxy to
127.0.0.1. - A port that must be public to a few IPs: fix 2, plus fix 1 for everything else.
- Every server, always: fix 3 as the safety net.
Quick checklist
- [ ]
docker psshows no0.0.0.0:ports except the reverse proxy (80/443) - [ ] Databases, admin panels and dashboards have no
ports:entry, or are bound to127.0.0.1 - [ ]
nc -zvfrom another machine fails for every port you didn't mean to open - [ ] A provider firewall allows only 22, 80 and 443
More hardening steps: Linux server hardening checklist and our open-source one-command hardening script.
Disclosure: we run VeloxaHost. Everything above works on any Ubuntu or Debian server running Docker with its default iptables backend.
Top comments (0)