A ports: ["5432:5432"] entry in a Compose file publishes the port by inserting rules into the iptables DOCKER chain, which is traversed before most host firewall rules configured with ufw or firewalld. A database that looks protected by a default-deny host firewall is in fact reachable from any network the host is attached to.
Confirm it from another machine with a TCP connect to the published port, or on the host with iptables -t nat -L DOCKER -n, which lists the DNAT entries Docker created.
There are three fixes. Bind to loopback explicitly with "127.0.0.1:5432:5432", which is correct when only host tooling needs access. Use expose instead of ports when only other Compose services need the port, since services on the same Compose network reach each other by service name without publishing anything. Or, on Docker Engine 28 and later, rely on the tightened default that binds published ports on unroutable addresses only when configured to; do not assume it, verify. This applies to Linux; Docker Desktop uses a VM and a different path.