localhost vs 0.0.0.0: Why Your Service Is Not Reachable
Test a small HTTP server to distinguish its bind address from the address a client uses, then check the remaining network path.

You can open a development server at http://localhost:18081, but someone on the same network cannot. The process is running and the port number is correct. The missing detail may be the address on which the process listens.
127.0.0.1 names this machine’s IPv4 loopback interface. A process bound only there accepts connections addressed to that loopback interface. 0.0.0.0 has a different role: for an IPv4 server, it means listen on all local IPv4 interfaces. It is not the destination you give a browser on another machine. That browser needs an address assigned to the server, such as its LAN address. The Linux ip(7) manual describes these wildcard and loopback addresses.
Run the same server with two bind addresses
Use a directory containing only a disposable test page. Python’s http.server command-line documentation supports an explicit --bind address and --directory. Its default is all interfaces, so specify the address in both runs to make the comparison visible.
mkdir -p /tmp/bind-address-demo
printf 'bind-address-demo\n' > /tmp/bind-address-demo/index.html
python3 -m http.server 18081 --bind 127.0.0.1 --directory /tmp/bind-address-demo
In a second terminal on the same machine, request the page:
curl -fsS http://127.0.0.1:18081/
The expected body is bind-address-demo. On Linux, inspect the listener with ss -ltn and find port 18081 in the local-address column. The ss manual explains that -l selects listening sockets and -n keeps numeric addresses and ports. A 127.0.0.1:18081 listener does not accept a connection addressed to the machine’s LAN IP. Stop the server with Ctrl-C.
Now bind the same server to all local IPv4 interfaces:
python3 -m http.server 18081 --bind 0.0.0.0 --directory /tmp/bind-address-demo
The local 127.0.0.1 request should still work. On Linux, the listening address should now appear as 0.0.0.0:18081 or *:18081, depending on the tool’s display. From a second device on a network you control, try http://SERVER_LAN_IP:18081/, replacing the placeholder with the server’s actual LAN address. Do not enter 0.0.0.0 in that device’s browser.
Binding to all interfaces permits the server to receive connections on them; it does not guarantee that another device can reach it. A host firewall, network isolation, router policy, or a wrong LAN address can still block the path. Test from the intended client rather than treating a successful request on the server itself as proof of remote access. Stop the server with Ctrl-C when finished; Python’s simple HTTP server is intended for controlled development tests, not production hosting.
Read the result at the right layer
| Observation | What it establishes | Next check |
|---|---|---|
curl to 127.0.0.1 succeeds |
The local application answers on loopback | Inspect the bound address |
Listener shows 127.0.0.1:18081 |
Only loopback is in scope for this IPv4 socket | Bind to the required interface if remote access is intended |
Listener shows 0.0.0.0:18081, but remote client fails |
The process accepts local IPv4 interface addresses | Check the client’s destination, route and filtering |
| Remote client gets a response | That specific path reaches the application | Check the actual response body, not only the port |
If the service runs inside a container, there is another boundary. Its bind address is inside the container’s network namespace, and the host may also need a published port. The Docker EXPOSE vs Publish experiment tests that separate mapping. For a plain Linux process, start with the socket address and the client’s destination; only then investigate the network between them.