Part of the open-ports cluster
This page answers one question: why a port that is plainly listening still refuses. The wider map (which process owns a port, how to see it without root, what docker-proxy hides, how to test from outside) is in the full guide: List Open Ports on Linux.
ss says the port is listening. curl on the server gets a 200. From your laptop, the same port answers Connection refused instantly, every time. The service is fine and the firewall is not involved. The service is bound to 127.0.0.1, and a loopback socket does not accept a connection addressed to any other IP, not even the server's own LAN address. The answer is in one column of ss output that most people read straight past.
What Does a Loopback-Only Listener Look Like?
Run here on 2026-09-28 (Kali, bash 5.3.9, iproute2 7.1.0, curl 8.21.0, Python 3.14.7). A test web server bound to loopback, the way most databases, caches and dev servers ship by default:
From the same machine, to 127.0.0.1:
From the same machine, to 127.0.0.2. Linux routes the whole 127.0.0.0/8 block to the loopback interface, so this is a different destination address on the same box, which is exactly what a connection to the server's LAN IP is from the socket's point of view:
after 0 ms. Not a timeout: the kernel answered immediately that nothing listens there. Newer curl prints "Could not connect to server"; curl -v and bash's own /dev/tcp show the underlying error by its usual name:
And the one line that explains it:
127.0.0.1:8097. Listening, and only for connections addressed to 127.0.0.1.
The Script
Save as check-bind-address.sh. It reads the same ss output, classifies every listener on the port, and exits with a code a script can branch on.
What Does the Script Print?
Against the loopback-bound server above:
The same server restarted with --bind 0.0.0.0, and the connection that was refused a minute ago:
With the server stopped, the other cause of an instant refusal:
And a port on this box that listens on both address families at once, which is a Docker-published container port (the IPv4 and IPv6 wildcards are two separate sockets):
How Do I Read the Local Address Column?
| Local Address | Who can connect |
|---|---|
127.0.0.1:PORT, 127.0.0.53%lo:PORT, [::1]:PORT | this machine only |
0.0.0.0:PORT | any IPv4 address the host has, including LAN and VPN |
[::]:PORT | any IPv6 address (and IPv4 too, when the socket shows v6only:0 in ss -e) |
*:PORT | both families on one socket |
192.168.x.y:PORT or any single address | only connections to that address |
The Open Ports Explainer applies this table to every row of a pasted ss, netstat or lsof listing.
"Could connect" is a statement about the socket, not about your network. A 0.0.0.0 listener behind a firewall, a NAT router or a cloud security group can still be unreachable from the outside, and nothing in ss can tell you that. Test from another machine with nc -zv HOST PORT. The guide's section on testing one port from outside covers it.
Where Is the Bind Setting for Common Services?
The fix is always in the service, never in ss. The setting names differ:
| Service | Setting | File |
|---|---|---|
| PostgreSQL | listen_addresses = '*' (plus a pg_hba.conf rule) | postgresql.conf |
| MySQL / MariaDB | bind-address = 0.0.0.0 | my.cnf / 50-server.cnf |
| Redis | bind 0.0.0.0 (and set requirepass) | redis.conf |
| Node / Express | app.listen(PORT, '0.0.0.0') | app code |
Python http.server | --bind 0.0.0.0 | command line |
| Vite dev server | --host | command line |
| Docker publish | -p 3000:8080 binds 0.0.0.0; -p 127.0.0.1:3000:8080 binds loopback | docker run / compose |
Restart the service after the change and re-run the script. If it still reports loopback only, the config file you edited is not the one the service loads: systemctl status SERVICE shows the command line, and ss -ltnp as root shows which process owns the socket.
How Do I Tell a Refusal From a Firewall Drop?
Time it. The loopback-bound refusal above failed after 0 ms. A firewall that drops packets leaves the client waiting for its connect timeout, which is seconds to minutes. An instant refusal means the packet arrived and nothing was listening on that address; a hang means something in the path discarded it. A REJECT --reject-with tcp-reset rule is the one firewall setup that also refuses instantly, so if the script reports every interface and remote clients still get an instant refusal, look at nft list ruleset or iptables -S for REJECT rules.
For the other half of the port question, which process is holding a port you did not expect, see Kill the Process Using a Port. To catch a listener that should not be there before someone else does, run ports-audit from cron. A check that runs unattended wants a lock and an alert when it exits non-zero. The Production Bash Toolkit packages those as bashlib.sh.
Frequently Asked Questions
Why does curl to localhost work but curl to the server's IP get connection refused?
The service is bound to 127.0.0.1. A loopback-bound socket only accepts connections addressed to the loopback address, so curl http://localhost:PORT reaches it and curl http://SERVER_IP:PORT does not, even when you run both on the same machine. ss -ltn 'sport = :PORT' shows 127.0.0.1:PORT in the Local Address column. Change the service's bind or listen setting to 0.0.0.0 or to the LAN address and restart it.
Is connection refused a firewall problem?
Usually not. A firewall rule that drops packets makes the client hang until it times out; it does not produce an instant refusal. An instant Connection refused means the packet reached the host and nothing was listening on that address and port, which is the loopback-bind case or a service that is not running. The exception is a REJECT rule with tcp-reset, so if the bind address is 0.0.0.0 and the refusal persists, check iptables or nftables for REJECT rules next.
What is the difference between 0.0.0.0 and 127.0.0.1 in ss output?
0.0.0.0:PORT is the IPv4 wildcard: the socket accepts connections arriving on every interface the host has, including the LAN and any VPN. 127.0.0.1:PORT accepts connections only from processes on the same machine. [::]:PORT and [::1]:PORT are the IPv6 equivalents, and *:PORT is a socket that listens on both families at once.
Is binding to 0.0.0.0 safe?
It exposes the service to every network the host is on, so it is only as safe as the service's own authentication and your firewall. Databases, caches and admin panels are loopback-bound by default for that reason. Prefer binding to the one LAN address that needs it, or keep loopback and reach the service through an SSH tunnel: ssh -L 5432:127.0.0.1:5432 user@server.
Why do I get connection refused for a Docker container port?
Either the port was never published with -p, so nothing on the host listens for it, or the application inside the container listens on 127.0.0.1, which is the container's own loopback and unreachable from docker-proxy. Run ss -ltn 'sport = :PORT' on the host to see whether a publish exists, then make the app inside the container listen on 0.0.0.0.
Part of the bash snippets collection
Related Scripts
- List Open Ports on Linux — every listening port with the process that holds it
- Kill the Process Using a Port — for "address already in use", the opposite problem
- Audit Listening Ports — alert once when a new listener appears
- Find Your IP Address on Linux — which LAN address a specific-address bind should use