Synthetic Industry

Troubleshooting guide · updated 2026-10-11

Your container port is open to the internet even though ufw says it is closed

How Docker-published ports sit outside the rules ufw manages, how to bind a port to the loopback address, and how to prove from outside which ports a server really exposes.

The surprise

A common situation: a server has a firewall configured to deny everything except SSH and web traffic, a database container is started with a published port, and a scan from outside finds the database port open anyway. Docker's own documentation explains why. Published container traffic is handled in the network address translation table and diverted before it reaches the input rules that ufw relies on, so ufw settings do not apply to published ports in the way people expect. The page describes the two tools as incompatible and warns against modifying the rules Docker creates.

  • ufw status can look correct and still not govern published container ports.
  • The risk is highest for databases, caches and admin panels.
  • The only reliable check is a connection test from another machine.

Publish less, and bind what you publish

The simplest control is to publish only what must be reachable. In Compose, a service on the same network can reach another service by name without any published port, and the expose attribute documents container ports for other services without publishing them to the host. When a port must be published for a process on the host, give a host address as well, for example 127.0.0.1 in front of the host and container ports, which the Compose reference says limits access to the loopback interface. Omitting the address binds all interfaces, and the reference warns that this can expose the container to the internet and bypass host firewall rules.

  • Prefer service-to-service names on the Compose network; publish nothing for internal services.
  • Quote the mapping so the YAML parses it as text.
  • Put the reverse proxy on 80 and 443 and keep everything else off the public interface.

If the matrix is wider than the box, scroll horizontally to read every column. Keyboard: focus the matrix and use Left/Right.

services:
  db:
    image: postgres:16
    # no ports: only other services on the Compose network can reach it
  app:
    image: registry.yourbusiness.co.uk/app:1.4.2
    ports:
      - "127.0.0.1:3000:3000"   # reachable by the proxy on this host only

Prove it from outside

Do not trust the configuration, trust a test. From a machine that is not the server, try to connect to each port you care about, including the database and cache ports, and record which accept a connection. Repeat on IPv6 if the server has an IPv6 address. List what the server itself reports as listening, and compare the two views: a socket listening on all addresses may or may not be reachable depending on the layers in front of it, and only the outside test settles that. Keep the table; it is the acceptance evidence for a deployment or a hardening job.

  • Test from a network that is not the server's own and not your office if the office is allow-listed.
  • Include UDP ports if any service uses them.
  • Record the date and the server address so the result can be repeated.

If the matrix is wider than the box, scroll horizontally to read every column. Keyboard: focus the matrix and use Left/Right.

port   expected   result from outside
22     open       open
80     open       open
443    open       open
5432   closed     closed
6379   closed     closed

Where a provider firewall helps

A network firewall in your provider's control panel sits in front of the server and is not bypassed by Docker's host rules. Using one for the same allow-list is a reasonable second layer, so a mistake in a Compose file does not expose a database. It does not replace the checks above, and it must also be kept in step with the ports the stack really needs. When you change the stack, re-run the outside test; do not assume.

  • Treat the provider firewall and the host rules as two layers with the same list.
  • Re-test after every change to ports or to the provider rules.

How the paid jobs use this

The Compose deployment job lists every port the stack publishes and tests each from outside before sign-off. The Linux baseline job does the same for the whole server and reports any container-published port separately, because the host firewall does not govern it. Neither job promises that a server cannot be attacked; they show which ports are reachable and that the list matches the one you agreed.

Sources and limits

  • Docker: packet filtering and firewalls Checked 2026-10-11.
    • Docker and ufw are described as incompatible: published container traffic is diverted in the nat table before the INPUT chain that ufw uses.
    • Docker advises not modifying the rules it creates.
  • Docker: Compose services reference (ports) Checked 2026-10-11.
    • A published port without a host IP binds on all interfaces; a host IP such as 127.0.0.1 limits access to loopback.
    • expose declares container-internal ports without publishing them to the host.
  • ufw manual Checked 2026-10-11.
    • ufw starts disabled with incoming traffic denied by default, and ufw status verbose shows the active rules.