Vite server running but not working on localhost: 3 causes
Tell in one request whether Vite itself answered 404 or another server did, then move index.html, pin the host or free the port, and confirm it with curl.
The terminal says Vite is ready and prints a Local: URL, yet the browser shows a "page can't be found" screen instead of your app. A Stack Overflow question with this exact symptom collected three answers that each fixed it for someone: move index.html, set the host to 127.0.0.1, or change the port. They are three different faults, and one request tells you which one you have.
TL;DR#
- Vite answers 404 with an empty body:
index.htmlis inpublic/. Move it to the project root. - Something else answers on that port: Vite bound
localhostto one loopback address and another program owns the other one. Setserver.host: '127.0.0.1'so the clash shows up, or change the port. - The port in your URL is not the one in Vite's
Local:line: you are on the wrong port. It is refused if nothing listens there, or shows another program's page if something does. Open the exactLocal:line; the default port is 5173, and Vite moves to another one when that port is taken.
The symptom, as the asker reported it#
This localhost page can’t be found No webpage was found for the web address: https://localhost:4200/
That page is the browser's rendering of an HTTP 404. A 404 means some server did answer; the question is whether it was Vite.
Why it happens#
Everything below was reproduced on 30 October 2026 on a Windows 11 host (Docker was not available for this run) with Node v26.7.0, Vite 8.3.5 from npm install -D vite@latest, and headless Chrome 156. Ports are random because each run picked a free one.
Cause 1: index.html is in public/#
Create React App keeps index.html in public/. Vite does not. The Vite guide says index.html is "front-and-central instead of being tucked away inside public", because during development "Vite is a server, and index.html is the entry point to your application". With the file left in public/, Vite starts normally and every page request falls through to its last middleware. In the Vite source that middleware is three lines: set statusCode to 404, call res.end(), nothing else.
The reproduction shows exactly that: a ready server, then a 404 with an empty body.
VITE v8.3.5 ready in 181 ms
➜ Local: http://localhost:64430/
➜ Network: use --host to expose
--- requests ---
GET http://localhost:64430/ -> 404 | body: ""
GET http://127.0.0.1:64430/ -> ECONNREFUSED
GET http://[::1]:64430/ -> 404 | body: ""An empty 404 gives the browser nothing to render, so it shows its own not-found page. That is consistent with the message in the question; the headless browser used here does not render that page, so the exact wording is the asker's, not part of the reproduction.
Cause 2: localhost resolves to one address, another server owns the other#
Look at the second request above: 127.0.0.1 was refused while [::1] reached Vite. The server.host option defaults to 'localhost', and Vite listens on whatever address the name resolves to first. Since Node 17 the default DNS order is verbatim, the order the operating system returns, and on this Windows host that put the IPv6 loopback first:
dns.lookup("localhost", {all:true}) = [{"address":"::1","family":6},{"address":"127.0.0.1","family":4}]So Vite held ::1 only. The Vite docs warn about the consequence directly: "There are cases when other servers might respond instead of Vite", and the first case "is when localhost is used", because browsers may use a different resolved address than the one Vite is listening to.
To show it, a second HTTP server that answers 404 with an empty body (and a marker header) was started on 127.0.0.1, then Vite was started on the same port. Vite did not notice the clash at all:
VITE v8.3.5 ready in 153 ms
➜ Local: http://localhost:63966/
➜ Network: use --host to expose
--- requests ---
GET http://localhost:63966/ -> 200 text/html | body: "<!doctype html>\n<html>\n<script type=\"module\" src=\"/@vite/cli"
GET http://127.0.0.1:63966/ -> 404 x-served-by=other-server | body: ""
GET http://[::1]:63966/ -> 200 text/html | body: "<!doctype html>\n<html>\n<script type=\"module\" src=\"/@vite/cli"Any client that goes to 127.0.0.1 gets the other program's response instead of Vite's. Here that was an empty 404, the same thing cause 1 produces; a real program on that port answers with whatever it serves. Chrome 156 on this host was not one of them: it connected over ::1 and rendered the app. The clients that do take the IPv4 path are the ones that use the IP explicitly: a bookmark or proxy target written as 127.0.0.1, curl, a backend fetch, or port forwarding. Vite's troubleshooting page names VS Code port forwarding, which does not support IPv6, and recommends 127.0.0.1 there.
Even --strictPort does not catch this case. With the same IPv4 squatter and --strictPort, Vite started on the requested port and the other server still answered on 127.0.0.1.
Cause 3: you are looking at the wrong port#
The docs give the default as server.port 5173, and Vite "will automatically try the next available port" when the port is taken. When another program holds the same address, Vite moves and says so:
Port 63970 is in use, trying another one...
VITE v8.3.5 ready in 163 ms
➜ Local: http://localhost:63971/A tab left open on the old port, a bookmark, or a URL copied from an earlier setup now points somewhere Vite is not. If the other program still holds that port, as here, you see whatever it serves; if nothing listens there, the connection is refused. Either way the test is the same: compare the port in the address bar with the one on Vite's Local: line. The third answer on the question found Docker Desktop on the port Vite was trying to use.
Fix#
Work through them in order; the first request tells you where to stop.
- Find out who answered. First compare the port in the URL you opened with the port on Vite's
Local:line. If they differ, that is cause 3, whatever the URL you opened returns. If they match, request that URL, then the same port on both loopback addresses:
curl -i http://localhost:5173/
curl -i http://127.0.0.1:5173/
curl -g -i "http://[::1]:5173/"A 404 with no body from the address Vite holds is cause 1. A response that is not your app on only one of the two addresses is cause 2. Connection refused on all three means Vite is not on this port at all: go back to the Local: line (cause 3).
- Move
index.htmlto the project root (cause 1). On macOS or Linux, from the project root:
mv public/index.html ./index.htmlKeep public/ for what the Vite assets guide puts there, such as files never referenced in source code (robots.txt) or files that must keep their exact name. The guide also notes that URLs inside index.html are rebased automatically, so there is no need for %PUBLIC_URL% placeholders. In the reproduction this one move turned the empty 404 into the app on the next run.
- Pin the host (cause 2). In
vite.config.js:
import { defineConfig } from 'vite'
export default defineConfig({
server: {
host: '127.0.0.1',
},
})The docs note the option "can be set via the CLI using --host", and the reproduction used that CLI form, --host 127.0.0.1. With that single change and the same IPv4 squatter in place, Vite now saw the clash and moved to a free port, and every IPv4 request reached it:
Port 62191 is in use, trying another one...
VITE v8.3.5 ready in 152 ms
➜ Local: http://127.0.0.1:62192/
--- requests ---
GET http://localhost:62192/ -> 200 text/html | body: "<!doctype html>\n<html>\n<script type=\"module\" src=\"/@vite/cli"
GET http://127.0.0.1:62192/ -> 200 text/html | body: "<!doctype html>\n<html>\n<script type=\"module\" src=\"/@vite/cli"
GET http://[::1]:62192/ -> ECONNREFUSEDNote the last line: [::1] is now refused. Pinning the host trades one loopback for the other, so open the printed 127.0.0.1 URL. The localhost request above still succeeded because Node's fetch tried the IPv4 address after ::1 was refused; do not assume every client does the same.
- Fix the port (cause 3). Either stop the other program, or keep the port fixed and make Vite fail loudly instead of drifting to another one:
import { defineConfig } from 'vite'
export default defineConfig({
server: {
port: 5173,
strictPort: true,
},
})The docs describe strictPort as "exit if port is already in use". Run with the --strictPort flag and a squatter on ::1, the reproduction ended with Error: Port 59886 is already in use instead of a silent move, which is what you want in a script that opens a fixed URL.
Verify the fix#
Request the printed Local: URL again. A healthy dev server returns 200 text/html, and the body is your index.html with Vite's client script added. The dev server's HTML transform injects a module script pointing at the /@vite/client path with injectTo: 'head-prepend', so it is the first child of <head>. The HTML plugin falls back to placing it right after <html> only when the file has no <head>, which is why the minimal test file above shows it there. If your config needs Node to resolve localhost the IPv4 way, NODE_OPTIONS=--dns-result-order=ipv4first reorders it; in the reproduction Vite then printed Local: http://127.0.0.1:60107/ instead of localhost, which is the "Vite prints the resolved address when it differs" behaviour the docs describe.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
When the 404 moves to the build#
Leaving index.html in public/ also breaks production builds. On Vite 8.3.5, vite build stopped with [UNRESOLVED_ENTRY] Cannot resolve entry module index.html.; a report from an older release worded the same failure Could not resolve entry module (index.html). If index.html is at the root locally but that error shows up in a Docker build, check whether a .dockerignore rule keeps it out of the build context: see the .dockerignore fix for Vite's index.html entry. A Create React App migration that links local packages can then stop on Vite's "does not provide an export named" error, which comes from Vite skipping CommonJS conversion for linked packages. If vite itself fails to load because npm install skipped devDependencies, see Cannot find package vite in vite.config.ts.
If Vite answers but the page never finishes loading in Chrome on Linux, the cause is different again: Chrome stalls on Vite's many module requests when it runs out of file descriptors. And if it is your Node backend, not the browser, that fails to reach a localhost service with ECONNREFUSED ::1, the same IPv6-first resolution is at work; the ECONNREFUSED ::1 fix applies the 127.0.0.1 change on the client side.
A running Vite server that shows "not found" is almost never broken. Either it answered 404 because it has no index.html at the root, or something else answered for it. One curl -i against both loopback addresses separates the two, and in both cases the fix is a single move or a single config line.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Fix error:0308010C digital envelope routines::unsupported
Find which part of your build still asks for MD4, apply the one-line workaround to unblock a deploy, then remove it with a webpack or react-scripts upgrade.
Fix secretOrPrivateKey must have a value in NestJS JWT
Reproduced on @nestjs/jwt 12.0.3 and 10.0.3: why a configured JwtModule still signs with an empty key, why two popular answers fall short, and the .env load-order variant of the same error.
Fix Invalid options object: allowedHosts[0] with CRA proxy
Find which of the two triggers crashes npm start, pick a fix that still proxies your API calls, and avoid the http-proxy-middleware v3 path trap.


