localhost vs 127.0.0.1 vs 0.0.0.0
localhost is a name, 127.0.0.1 is the IPv4 loopback address that name usually resolves to, and 0.0.0.0 is the address a server uses to mean “any address” when it binds. A server bound to 127.0.0.1 accepts connections only from the same machine. A server bound to 0.0.0.0 accepts them on every IPv4 address the machine has, so your phone, a virtual machine, and a colleague on the same network can connect too, unless a firewall or the network stops them.
Who can reach what
The bind address is the setting that decides this. It is not the address in the URL you type.
| Server binds to | Same machine | Other devices | Notes |
|---|---|---|---|
127.0.0.1 | Yes, at 127.0.0.1 | No | IPv4 loopback. Django’s runserver default. |
::1 | Yes, at [::1] | No | IPv6 loopback. A listener on 127.0.0.1 does not answer here. |
localhost (in a config) | Yes | No | The name is resolved at startup, so the address depends on your resolver. Vite’s default. |
0.0.0.0 | Yes | Yes, if nothing blocks them | Any IPv4 address. |
:: | Yes | Yes, if nothing blocks them | Any IPv6 address. Node says it may also accept IPv4. |
A LAN address, such as 192.168.1.20 | Only at that address | Yes, at that address | Changes when the router hands out a new one. |
What each one is
localhost is a name
localhost goes through name resolution like any hostname. Node’s dns.lookup() hands the name to the operating system, which reads files such as /etc/hosts, and the Node documentation notes that its dns.resolve*() functions do not. To see what your machine answers, run:
import dns from "node:dns/promises";
console.log(await dns.lookup("localhost", { all: true }));Each entry is one address with a family of 4 or 6. If you see both, the order matters, and the IPv6 section below explains why.
127.0.0.1 is loopback
The Linux IP documentation says INADDR_LOOPBACK always refers to the local host through the loopback device. Traffic to it never reaches your network, which is why other devices cannot connect. It also means that on your phone, 127.0.0.1 and localhost point at the phone.
0.0.0.0 is a bind address
The same documentation says INADDR_ANY means any address for binding. Use 0.0.0.0 in server settings, and use 127.0.0.1 or the machine’s LAN address in the URL you open. Some servers do this without being asked. When you give Node no host, it listens on :: if IPv6 is available and on 0.0.0.0 otherwise, and listening on :: may also take IPv4. A bare server.listen(3000) is therefore not limited to loopback.
Defaults differ by tool, and they are the usual reason a phone works with one project and not another:
Why your phone cannot open it
- Bind to an address other devices can reach. For Vite, run
vite --host. - Open the computer’s address, not localhost. On your phone, use
http://192.168.1.20:5173, with your computer’s own address from its network settings. - Test from the computer first. Open that same address in the computer’s browser. If it fails there, the server still listens on loopback only.
- If the computer succeeds and the phone fails, the bind is right, and the cause is a firewall or the network between the two. Rig’s firewall and network page notes that guest Wi-Fi isolation blocks QR previews.
Binding is necessary, but the framework can still say no. Vite’s server.allowedHosts lists the hostnames it responds to, and by default it allows localhost, .localhost domains, and all IP addresses, so a custom hostname or tunnel domain is refused until you add it. Django’s ALLOWED_HOSTS works the same way. With DEBUG on and the list empty, Django validates the host against .localhost, 127.0.0.1, and [::1] only, so a request to the LAN address raises SuspiciousOperation until you add it:
ALLOWED_HOSTS = ["192.168.1.20"]Next.js has a similar gate. It blocks cross-origin requests to dev-only assets and endpoints by default, and allowedDevOrigins in its config lists the extra origins that may make them.
On WSL2, Vite’s documentation adds that setting host: true is not enough to reach the server from your LAN.
IPv6 and the Node 17 change
Node 17 changed dns.lookup() to return addresses in the order the resolver gave them, where earlier versions put IPv4 first. If your system’s resolver returns ::1 before 127.0.0.1 for localhost, a Node client now tries ::1 first, and a server bound only to 127.0.0.1 refuses it.
Node 20 and 18.18 softened this for outgoing connections. The autoSelectFamily option defaults to true there, so net.connect() tries the addresses in turn, alternating between IPv6 and IPv4, until one connects. The change does not cover browsers or other tools, and it does not change what your server binds. Vite’s documentation says browsers may use a different resolved address than the one Vite listens on, and that Vite prints the resolved address when it differs.
To check each family yourself:
curl -sS -o /dev/null -w "%{http_code}\n" http://127.0.0.1:3000/
curl -sS -o /dev/null -w "%{http_code}\n" "http://[::1]:3000/"A code from one and a failure from the other shows which family your server listens on. To fix a mismatch, use the same literal on both sides, bind both families (in Node, omit the host or use ::, which on most systems covers both), or set the lookup order with node --dns-result-order=ipv4first or dns.setDefaultResultOrder("ipv4first").
Firewalls
Binding decides which addresses the server accepts. A firewall on the machine, or on the network, decides which packets arrive at all, so a server on 0.0.0.0 can still be unreachable. Firewall menus change between releases, so check your operating system’s own documentation and test from the phone after each change.
A security note
Binding to all addresses publishes the dev server to everyone who can reach the machine, including other people on a shared network. Dev servers are meant for development, not exposure. The Flask documentation warns that its debugger allows executing arbitrary Python code from the browser and is a major security risk, and tells you not to run the development server or debugger in production. Docker has the same trap: a published port with no IP address is available to all host addresses, and 127.0.0.1:8080:80 limits it to the Docker host.
Bind to all addresses only on a network you trust, and switch back to loopback when you are done.
How Rig treats these addresses
These facts come from Rig’s code:
- Port checks: Rig tries to bind
127.0.0.1and::, and a failure on either counts as taken. - Health checks: when a project’s URL uses
localhost, Rig connects to127.0.0.1, so a server listening only on::1does not answer. - MCP hub: it listens on
127.0.0.1, port 47823 by default. An optional second listener on the Mac’s mesh address serves a paired iPhone, and it is off on fresh installs. - Local HTTPS: the proxy only fronts loopback upstreams. See Local HTTPS.
- LAN share: it shows a QR code for the local network URL and needs no account. See LAN share with QR.
Which one to use
localhost: URLs you open yourself, when either IP version is fine.127.0.0.1: health checks, proxy upstreams, and test clients, where you want IPv4 and no resolver surprises.[::1]: only when you are deliberately testing IPv6.0.0.0.0or::: server settings, when a phone, a container, or a colleague must connect.- A LAN address: when you want one network interface and no others.
For port clashes, see find what is using a port and free it. The glossary covers localhost, the loopback address, and the network port.
Frequently asked questions
Is localhost the same as 127.0.0.1?
Not always. localhost is a name that your resolver turns into one or more addresses, usually 127.0.0.1 and ::1. Typing 127.0.0.1 skips resolution and picks IPv4.
Why does my server work on 127.0.0.1 but not on localhost?
The name probably resolves to ::1 first, and the server listens only on IPv4. Run the dns.lookup() snippet above, then bind both families or use the same literal everywhere.
How do I open my dev server on my phone?
Bind to all addresses, for example with vite --host, then open the computer’s address on the shared network, such as http://192.168.1.20:5173. Check your framework’s allowed-hosts setting if the page is refused.
Is it safe to bind a dev server to 0.0.0.0?
Only on a network you trust. Every device that can reach the machine can reach the server, and dev tools such as the Flask debugger allow arbitrary code execution.
Does Node listen on IPv4 and IPv6 by default?
With no host given, Node listens on :: when IPv6 is available, and in most operating systems that may also take IPv4. Without IPv6 it listens on 0.0.0.0.
Sources
- Node.js: dns.lookup, order option and default result order
- Node.js: net, server.listen default host and autoSelectFamily
- Linux man-pages: ip(7), INADDR_ANY and INADDR_LOOPBACK
- Vite: server options, host, allowedHosts
- Vite: command line interface
- Next.js: next CLI, hostname option
- Next.js: allowedDevOrigins
- Django: django-admin runserver
- Django: ALLOWED_HOSTS setting
- Flask: quickstart, externally visible server
- Rails Guides: the command line
- Docker: port publishing and mapping
- Rig documentation: firewall and network (guest Wi-Fi isolation note)
- Rig documentation: LAN share with QR
Run your whole stack from one window
Rig is a Mac app that starts, stops, and watches the processes in your project, with logs and ports in one place. Local use is free, and your source code stays on your Mac.