You deploy a dashboard, set UFW to deny incoming traffic and allow only SSH and HTTPS. Then a browser can still reach the dashboard on port 8080. That is a configuration problem worth investigating before you deploy a database, cache or administration panel in the same way.
This is a practical hardening guide for your own servers, not a reason to panic about every container. We will inspect what Docker publishes, distinguish host-based and container-based proxies, and build a repeatable verification checklist. HYEHOST Cloud VPS gives you the operating-system control to configure this deliberately; that control also means the application exposure policy remains yours to manage.
1. Why Docker ports can bypass UFW
Docker creates Linux firewall rules for bridge-network forwarding and port mapping. Published traffic can be translated and forwarded to a container before the host INPUT rules you expected UFW to enforce. Docker describes this interaction in its firewall documentation.
That does not mean UFW is useless. It means you must distinguish traffic terminating on the host from traffic forwarded to a container. A “deny incoming” summary is not a complete description of both paths.
In particular, this Compose mapping does not mean “private until I add a UFW allow rule”:
ports:
- "8080:80"
It publishes host port 8080 to container port 80 without an explicit host address. Treat it as potentially public, then confirm the actual bindings and upstream filtering. A working web page at that port proves reachability; an authentication screen does not prove the service should have been public in the first place.
2. Audit the running deployment before editing rules
Keep an existing SSH session open, confirm console or recovery access, and save your current configuration before changing networking. Do not flush firewall chains or reset UFW as a troubleshooting shortcut. If a change can affect customers, schedule a maintenance window and know how to restore the previous configuration.
Start with read-only checks on the VPS:
docker version
docker ps --format "table {{.Names}}\t{{.Ports}}"
docker compose ps
sudo ufw status verbose
sudo ss -lntup
Run Compose commands in the correct project directory with the same configuration files used for deployment. List published mappings for a specific container, replacing the example name:
docker port YOUR_CONTAINER
Review both the configured intent and the live result. A Compose edit does not change a container until you apply it. Also, a socket listing alone can miss exposure implemented through packet translation rather than a conventional listening process. Use Docker's view and an external test as well.
docker compose config can help reveal merged overrides and substituted values. Its output may include secrets from the configuration: inspect it locally and do not paste it wholesale into a public ticket. A second Compose file can reintroduce a port that the main file appears to remove.
| What you find | What to investigate |
|---|---|
| 0.0.0.0:8080 | An all-IPv4-address binding; check whether external clients can reach it. |
| [::]:8080 | An IPv6 wildcard binding; verify IPv6 access independently. |
| 127.0.0.1:8080 | A host-loopback mapping, suitable for a host-based proxy in the standard setup described here. |
| 80/tcp with no host mapping | Container port information, not by itself a published host port. |
| No mapping but host networking | The process uses the host network namespace; inspect its listeners and host policy instead. |
3. Choose who actually needs to reach each service
Write down a small access policy before deciding on commands. The useful question is not “which firewall tool is best?” but “which clients should reach this service, over which path?”
| Service | Intended access | Starting pattern |
|---|---|---|
| Public web application | Visitors over HTTPS | Expose the reverse proxy; keep the application backend private. |
| Database or cache | Only the application | No host publication; use a scoped container network. |
| Management dashboard | Administrators | VPN or SSH forwarding, plus application authentication. |
| Cross-VM backend | Named application VMs | Private networking with explicit filtering and authentication. |
| Public non-HTTP service | Specific external clients or a documented public audience | A deliberate published port and backend-appropriate access policy. |
Do not expose a database merely because a sample Compose file includes a ports block. Equally, do not assume that removing a web port makes every other path safe: review additional interfaces, VPN routes and containers that share the service's network.
4. Host-based reverse proxy: bind the backend to localhost
If Nginx or another reverse proxy runs directly on the VPS host, a localhost binding is a straightforward pattern. This complete demonstration uses Nginx only as a small test backend; adapt the container port and image to your application.
services:
backend:
image: nginx:stable-alpine
restart: unless-stopped
ports:
- "127.0.0.1:8080:80"
Save this in a separate test project's compose.yaml, validate it, and start it only if local port 8080 is free:
docker compose config --quiet
docker compose up -d
curl --fail --max-time 5 http://127.0.0.1:8080/
Configure the host proxy to reach http://127.0.0.1:8080. Our Nginx reverse-proxy guide covers the proxy side. The image tag above follows a release channel; use a reviewed version or digest and an update process for a production deployment.
Docker's port-publishing reference documents loopback bindings and warns that releases older than 28.0.0 had a same-L2-network exception. Use a maintained Engine release. Direct routing and non-default gateway modes can also change the access model; do not apply the localhost conclusion blindly to those configurations.
For an existing service, changing its mapping generally requires recreation, not just restarting the old container. Validate the merged configuration, preserve persistent data and apply the change with the appropriate Compose deployment workflow. Test the public endpoint and local backend afterwards.
5. Container-based reverse proxy: use the service name
A proxy container has its own loopback interface. Its 127.0.0.1 is not the VPS host and not another container. If the proxy and application are separate containers, connect them to a suitable shared network and target the application by service name and container port.
For example, this is a complete private test backend with no host port publication:
services:
backend:
image: nginx:stable-alpine
restart: unless-stopped
networks:
- proxy_backend
networks:
proxy_backend: {}
To extend it, attach your existing proxy service to proxy_backend in the same Compose project and configure its upstream as http://backend:80. Publish only the proxy's intended entry points, usually HTTP and HTTPS for a public web application. Separate Compose projects need an explicitly shared network rather than relying on identical YAML labels.
Compose provides service-name discovery on shared networks; see Docker's Compose networking guide. Keep unrelated applications off a broad shared network when they do not need to communicate. Network membership is part of the access design.
expose does not publish a host port, and it is not a firewall allowlist. The Compose service reference distinguishes it from ports. Merely replacing ports with expose does not restrict which peers on the same network can connect.
If you use internal: true on a network, account for the resulting external-connectivity restrictions. Some backends need outbound API calls or updates. Do not add it indiscriminately, and do not treat it as a substitute for authentication or careful network membership.
6. Keep administration interfaces off the public internet
For an occasional dashboard visit, you may not need a public HTTPS endpoint at all. With the backend bound to host loopback as above, SSH local forwarding gives an administrator a controlled access path.
Run this on your own computer, replacing the username and address with your VPS details. Local port 18080 must be unused:
ssh -N -o ExitOnForwardFailure=yes -L 127.0.0.1:18080:127.0.0.1:8080 YOUR_USER@YOUR_VPS_IP
Then open http://127.0.0.1:18080 on that computer. The SSH session must stay connected, and the SSH server must permit forwarding. Keep the application's login enabled; the tunnel limits the network path but does not make every local user or SSH account an authorised application administrator.
For regular team access, consider a properly configured VPN. Our Tailscale VPS guide describes another private-access approach. Review who can join the network and which services they can reach; “inside the VPN” should not automatically mean access to everything.
7. When filtering is needed, match Docker's firewall backend
Some services genuinely need a published port with source restrictions. In that case, identify Docker's actual firewall backend, the public interface, address families and existing rules before introducing a policy. Having the nft command installed does not establish which backend Docker uses.
Docker's iptables backend
The DOCKER-USER chain is the documented place for user filtering ahead of Docker's forwarding rules. By that stage, destination translation has already happened; matching the original published destination can require connection tracking. Read the iptables backend documentation before assuming a host-port match means the same thing at every hook.
Useful read-only inspection commands, when this is the backend you use, include:
sudo iptables -S DOCKER-USER
sudo iptables -t nat -S DOCKER
sudo ip6tables -S
Build rules around your actual traffic requirements, including established connections and legitimate forwarding. Broad example DROP rules copied from a different server can break containers, VPNs or routing. Save and test both the live policy and its persistence across service restarts and reboots.
Docker's native nftables backend
Native nftables uses a different integration model: there is no equivalent Docker-created DOCKER-USER chain. Policies need appropriate separate tables, hook points and priorities. Docker currently labels this backend experimental in its nftables documentation; check the guidance for your installed release.
Do not confuse the native backend with iptables-nft, the compatibility layer that lets iptables commands use nftables internally. Do not edit Docker-owned tables directly or switch backends mid-troubleshooting without a migration and recovery plan.
Disabling Docker's rule management with iptables: false is not a general UFW fix. It can break expected networking and isolation unless you provide a complete replacement design. Reducing unnecessary publication is usually a smaller, clearer first change than replacing the host's whole forwarding policy.
8. Verify from outside the VPS, over IPv4 and IPv6
A request from the VPS to itself does not prove what an internet client can reach. Test from another network that is not using your administrative VPN. Only test addresses and services you own or are authorised to assess.
For the HTTP demonstration, replace these documentation-only addresses with the VPS's real public addresses:
curl -4 --connect-timeout 5 --max-time 10 http://192.0.2.10:8080/
curl -6 --connect-timeout 5 --max-time 10 'http://[2001:db8::10]:8080/'
Once the backend is correctly private, these external direct-port requests should not return the backend page. Meanwhile, the local request or the intended HTTPS proxy path should still work. A timeout can have several causes, so combine the result with configuration inspection rather than treating one failed request as proof of a perfect firewall.
Use a client with working IPv6 for the IPv6 check. Testing a hostname may go through a CDN or resolve to a different machine; use the correct origin addresses when checking direct exposure. Absence of an AAAA record does not prevent someone connecting to a known IPv6 address.
HTTP tests cover this HTTP example only. Check other services using their own clients or an authorised port-testing tool, including UDP where applicable. Recheck after Compose changes, runtime upgrades and a controlled reboot. An access policy that disappears on restart is not finished.
Where HYEHOST fits into the design
Use a Cloud VPS when you want to manage Docker, the reverse proxy and Linux firewall yourself. For several services, VPS Resource Pools let you place workloads in separate VMs and maintain a staging environment before applying network changes to production.
Where services communicate across VMs, private networking can separate that traffic from public entry points. Bind deliberately, filter the intended clients and retain authentication. A private address by itself is not a complete trust policy.
HYEHOST's network DDoS protection is a different layer from deciding whether an application should be publicly reachable. It does not secure an exposed database, replace passwords or repair a permissive firewall. Keep backups independent too: Storage Box can provide an off-server destination, but backups do not prevent an exposure in the first place.
The final container-port checklist
- Every published port has a documented reason and intended audience.
- Databases and caches have no unnecessary public host mappings.
- Host-based proxies use the intended local backend; container proxies use the correct shared network.
- Administrative interfaces require private access and authentication.
- The installed Docker version and firewall backend are known.
- IPv4 and IPv6 are both tested from an external network where available.
- Expected services still work after recreation and a controlled restart.
- Recovery access, persistent data backups and the previous configuration are available.
Docker and UFW FAQs
Does Docker bypass UFW?
Docker-published bridge-network ports can remain reachable despite UFW incoming rules because their traffic follows Docker's forwarding and NAT path. Check published bindings and external reachability rather than relying only on ufw status.
What is the simplest way to keep a Docker service private?
If only other containers need it, omit the host port publication and connect the necessary containers on an appropriate network. If a host-based proxy needs it, use a localhost binding and verify the result externally. Direct-routing and nonstandard network modes need separate review.
Does expose in Docker Compose publish a port to the internet?
No. The expose setting does not create a host port mapping. It is also not an access-control list: containers sharing a network can generally reach a listening service without it.
Should I disable Docker's iptables rules to fix UFW?
Not as a general workaround. Disabling Docker's firewall rule management can break networking and remove expected isolation unless you supply a complete replacement policy. Prefer reducing published ports and designing backend-appropriate filtering.
Does DOCKER-USER work with Docker's native nftables backend?
No. DOCKER-USER is an iptables-backend mechanism. Docker's native nftables backend needs a separately designed nftables policy. An iptables-nft compatibility tool on the host does not by itself mean Docker uses the native nftables backend.
Does VPS DDoS protection secure an exposed database?
No. Network DDoS protection does not replace access controls, authentication, software updates or a correct firewall policy. Keep databases and management services private unless there is a deliberate, restricted access requirement.

