UFW is useful, but Docker-published ports can still surprise Linux server operators. On Linux, Docker creates firewall rules for bridge networks and port publishing. Docker’s documentation says Docker and UFW use firewall rules in ways that make them incompatible for published container traffic, because packets can be routed before UFW’s ordinary INPUT and OUTPUT chains apply.
The safe response is not panic. The safe response is to audit what Docker publishes, bind private apps to localhost, and treat UFW as one layer rather than the only exposure control.
The short rule
Do not assume this means a Docker-published port is private:
sudo ufw status
Also check Docker and listening sockets:
docker ps --format 'table {{.Names}}\t{{.Ports}}'
ss -ltnp
If Docker shows 0.0.0.0:PORT->..., treat that port as intentionally public or fix it.
Why UFW can be misleading with Docker
UFW is a friendly frontend for Linux firewall rules. Docker also manages firewall rules so container networking, NAT, and published ports work.
Docker’s firewall documentation explains that published container traffic is handled in the nat table and can be diverted before it reaches the chains UFW normally uses. The result: a port may be reachable even when a simple reading of UFW rules makes you think it should not be.
This is especially important on Ubuntu VPS machines where UFW is common and Docker is installed later.
The safer design pattern
For admin tools and internal apps, avoid direct public Docker publishes.
Prefer this:
services:
app:
image: example/app:latest
ports:
- "127.0.0.1:8080:80"
Then put one intentional access layer in front:
- Nginx, Caddy, or Traefik on the same host;
- Cloudflare Tunnel for public HTTP without direct origin ports;
- Tailscale or WireGuard for private admin access;
- SSH local port forwarding for temporary maintenance.
Avoid this unless the app should be directly reachable from the network:
services:
app:
image: example/app:latest
ports:
- "8080:80"
The second form can publish to all host interfaces by default.
Audit checklist for an existing server
Start with read-only checks.
1. List Docker-published ports
docker ps --format 'table {{.Names}}\t{{.Ports}}'
Look for lines like:
0.0.0.0:8080->80/tcp
That is the first signal to investigate.
2. Check local sockets
ss -ltnp
This shows what is listening locally. Treat 0.0.0.0:PORT as broader than 127.0.0.1:PORT.
3. Check UFW, but do not stop there
sudo ufw status verbose
UFW still matters for ordinary host services such as SSH or a reverse proxy. It just may not tell the whole story for Docker-published traffic.
4. Check from outside, only for systems you own
From a separate machine or an allowed scanner, test the public IP:
nmap -Pn -p 8080 SERVER_PUBLIC_IP
If the port is open and you did not intend it to be public, fix the Docker publish first.
Fix the common case: bind the container to localhost
If an app only needs to be reached by a local reverse proxy or tunnel, change the publish from this:
ports:
- "8080:80"
to this:
ports:
- "127.0.0.1:8080:80"
Then recreate the container during a maintenance window:
docker compose up -d
After it restarts, re-run:
docker ps --format 'table {{.Names}}\t{{.Ports}}'
ss -ltnp
Do not close your operational session or remove the old access path until the intended route works.
When direct Docker publishing is okay
Direct publishing can be fine for a deliberate public service, for example a web app that is meant to be exposed and has its own safe deployment path.
Even then, document:
- why the port is public;
- who owns the service;
- what authentication protects it;
- whether it terminates TLS directly or sits behind a proxy;
- when the exposure should be reviewed.
Public by accident is the problem.
What not to do casually
Do not disable Docker firewall management on a production server without a replacement plan
Docker’s documentation warns that preventing Docker from creating firewall rules is not appropriate for most users and is likely to break container networking unless you replace the rules correctly.
Do not edit Docker-created firewall chains by hand as your first move
Docker relies on its own rules for bridge networking and published ports. Manual rule surgery can create fragile behavior that breaks during Docker restarts or package updates.
Do not rely on one view of exposure
Use Docker output, local socket output, UFW status, cloud firewall rules, and an outside-in check together.
Related TheLinuxForum guides
- Linux Server Security
- Docker Ports: 0.0.0.0 vs 127.0.0.1 on Linux
- How to Check What Ports Are Listening on Linux
- UFW vs nftables vs firewalld
- How to Keep a Simple Public Service Inventory for a VPS
Sources
- Docker Docs: Packet filtering and firewalls — https://docs.docker.com/engine/network/packet-filtering-firewalls/
- Docker Docs: Docker and UFW — https://docs.docker.com/engine/network/packet-filtering-firewalls/#docker-and-ufw
- Docker Docs: Publishing and exposing ports — https://docs.docker.com/get-started/docker-concepts/running-containers/publishing-ports/
- UFW manual page — https://manpages.ubuntu.com/manpages/noble/man8/ufw.8.html
- Linux
ssmanual — https://man7.org/linux/man-pages/man8/ss.8.html
Verification note
This page is source-checked against Docker documentation and uses read-only audit commands where possible. It does not recommend disabling Docker firewall management as a first fix. Test any Compose changes during a maintenance window and verify from a second network path you control.
Bottom line
UFW is not enough by itself to reason about Docker exposure. Check Docker’s published ports, bind private apps to localhost, and expose only the one public path you actually mean to operate.