npm run dev —host does not expose your dev server. npm treats —host as its own argument and never forwards it. The command you want is npm run dev — —host, with a bare double dash that means everything after this belongs to the script.
This one costs people twenty minutes, reliably, because it fails without an error. The server starts, prints its usual local URL, and simply does not offer the network address you were after. Nothing is broken, so there is nothing to search for except the flag itself.
Table of contents
- The double dash is the whole fix
- What —host actually changes
- When the flag is right and it still does not work
- HMR, HTTPS, and the parts that break once you expose it
- Do not leave it bound to 0.0.0.0 by habit
- How this fits the rest of the stack
- FAQ
The double dash is the whole fix
npm run accepts its own options, and it parses the command line before handing control to your script. An unrecognised --host gets consumed rather than passed along. The bare -- separator marks the boundary.
# Silently does nothing
npm run dev --host
# Correct: everything after -- goes to the script
npm run dev -- --host
# With a specific address and port
npm run dev -- --host 0.0.0.0 --port 3000
Other package managers made different choices, which is a common source of confusion when someone switches:
npm run dev -- --host # npm needs the separator
pnpm dev --host # pnpm forwards unknown flags
yarn dev --host # yarn forwards them too
bun run dev --host # bun forwards them too
So a command copied from a colleague on pnpm genuinely does not work on npm, and the difference is invisible. If you are on npm and a flag seems to be ignored, the separator is the first thing to try.
The habit worth forming: put it in package.json instead of typing it. A flag that lives in the repo works for everyone, on every package manager.
{
"scripts": {
"dev": "vite",
"dev:host": "vite --host"
}
}
What —host actually changes
By default most modern dev servers bind to localhost, which resolves to the loopback interface. Traffic from any other machine never reaches it — not your phone, not a colleague, not a container on the same host.
--host with no value binds to 0.0.0.0, meaning every network interface. Vite then prints both addresses:
VITE v5.4.2 ready in 412 ms
-> Local: http://localhost:5173/
-> Network: http://192.168.1.24:5173/
That Network line is the point of the exercise. Open it on a phone connected to the same wifi and you are testing on real hardware instead of a resized browser window — which is the only way to catch the things that actually differ: touch targets, viewport units against a real address bar, iOS Safari’s opinions about fixed positioning.
You can also bind to one specific interface rather than all of them, which is the safer habit on a shared network:
npm run dev -- --host 192.168.1.24
And you can make it the default in the config file, where it is reviewable:
// vite.config.js
export default {
server: {
host: true, // same as --host
port: 5173,
strictPort: true // fail loudly instead of drifting to 5174
}
}
strictPort is underrated. Without it, a port collision makes the server quietly pick the next free number, and you spend a while wondering why your phone cannot reach the URL you memorised.
When the flag is right and it still does not work
If the Network line appears but the phone cannot load it, the dev server is doing its job and something between the two devices is not.
- Client isolation on the wifi. Guest networks and many office access points block device-to-device traffic by design. Both devices have internet and cannot see each other. Test by pinging the laptop from the phone.
- The host firewall. macOS asks for permission the first time and remembers a refusal. Windows Defender blocks inbound connections on networks marked Public by default — this is the single most common cause on Windows.
- Different networks. The phone is on 5GHz guest and the laptop on 2.4GHz main, or the phone quietly fell back to mobile data.
- Running inside WSL2 or a container. The server is bound to the virtual machine’s interface, not the host’s. WSL2 needs a port proxy; Docker needs an explicit
-p 5173:5173publish, and the process inside must bind to0.0.0.0rather than127.0.0.1.
Confirm what is actually listening before blaming the network:
# macOS / Linux
lsof -nP -iTCP:5173 | grep LISTEN
# Anywhere with ss
ss -ltnp | grep 5173
An address of 127.0.0.1:5173 means the flag did not take effect. 0.0.0.0:5173 or *:5173 means it did, and the problem is downstream.
HMR, HTTPS, and the parts that break once you expose it
Hot module replacement opens its own WebSocket connection back to the dev server, and it derives that address separately from the page URL. Exposing the server on the network is the moment HMR starts failing quietly — the page loads on the phone, but edits stop appearing.
The fix is to tell the client where to connect:
// vite.config.js
export default {
server: {
host: true,
hmr: {
host: '192.168.1.24',
port: 5173
}
}
}
The other thing that changes is the browser’s security posture. localhost is treated as a secure context even over plain HTTP, so service workers, the clipboard API, geolocation, camera access and crypto.subtle all work. A LAN IP address is not a secure context, and those APIs silently refuse. If a feature works on your laptop and is missing on the phone, this is usually why, and it is not a bug in your code.
Vite can issue a self-signed certificate to get you back into secure-context territory, at the cost of a browser warning you have to accept once per device:
npm install -D @vitejs/plugin-basic-ssl
npm run dev -- --host --https
Do not leave it bound to 0.0.0.0 by habit
A dev server is not a production server, and it was never written to be one. Binding to 0.0.0.0 publishes it to every device on the network — the coffee shop, the co-working space, the conference wifi.
What is on the other end is worth thinking about for a second. Dev servers serve source files, source maps, and the contents of your project directory. Vite has had path-traversal advisories in its file-serving layer. Your .env sits in that directory. And the HMR WebSocket accepts messages from whoever connects.
None of that is alarming on your home network. It is a genuinely bad idea on shared wifi.
- Prefer a named script (
npm run dev:host) over making exposure the default, so it is a deliberate act. - Bind to one interface rather than all of them when you can.
- For sharing with someone not on your network, use a tunnelling tool rather than opening a router port — it is scoped, revocable, and encrypted.
- For a genuine review link, deploy a branch. A preview URL survives your laptop closing, which a tunnel does not.
How this fits the rest of the stack
Most reasons for reaching for —host are really reasons for wanting a URL other people can open: showing a client, checking something on real hardware, letting a colleague reproduce a bug. A dev server on your laptop is a poor fit for that. It dies when the lid closes, it serves your source tree, and the address changes whenever DHCP feels like it.
A deployed branch does the same job properly. On RunxBuild, a static site or a service builds from your GitHub repo and comes up on a real route with a certificate, build logs and runtime logs — something you can send to someone and still have working tomorrow. Static sites include 120GB of bandwidth, and $0.10/GB after that. To see what a preview environment plus its API and database actually add up to, the RunxBuild hosting calculator lists each piece separately.
Useful related references:
- Run NextJS App Locally: The dev Command, the Build, the Start, the .env, and the One Mistake That Breaks the Localhost
- server.js Not Included in npm run build: What Next.js Produces, What You Have to Ship, and Where It Actually Lives
- npm Not Found: Every Reason It Happens and the Fix for Each One
- Services on RunxBuild
FAQ
Why does npm run dev —host do nothing?
npm parses its own arguments first and consumes --host instead of forwarding it to your script, with no warning. Use npm run dev -- --host: the bare double dash tells npm that everything after it belongs to the script. pnpm, yarn and bun forward unknown flags automatically, which is why a command copied from a colleague can fail on npm alone.
What does the —host flag actually do?
It binds the dev server to 0.0.0.0 — all network interfaces — instead of localhost, which is loopback only. The server then prints a Network URL alongside the Local one, and other devices on the same network can reach it. You can also pass a specific address to bind to just one interface.
My phone still cannot reach the dev server. What now?
Check that the process is really listening on all interfaces with lsof -nP -iTCP:5173 | grep LISTEN — if it shows 127.0.0.1, the flag did not apply. If it shows 0.0.0.0, look at the host firewall (Windows blocks inbound on Public networks by default), client isolation on the wifi, or whether the phone is on a different network.
Why does hot reload stop working when I use —host?
The HMR WebSocket derives its own connection address separately from the page URL, so it often still points at localhost. Set server.hmr.host in your Vite config to the LAN address you are actually using. This is also why edits stop appearing while the page itself keeps loading fine.
Is it safe to expose the dev server on my network?
On a trusted home network, generally yes. On shared or public wifi, no — a dev server serves your source files, source maps and project directory, and it was never hardened for untrusted clients. Prefer a dedicated script so exposure is deliberate, bind to one interface where possible, and use a tunnel or a deployed preview for sharing beyond the room.