Vite HMR Over an HTTPS Tunnel — Fix Hot Reload in 5 Minutes
The symptom
You run Vite locally. Hot module reload works on localhost:5173. You start a tunnel, open the HTTPS URL on your phone or share it with a teammate, and the page loads — but edits no longer hot-reload. Sometimes the browser console shows WebSocket connection failures or host validation errors.
The app works. HMR does not. This is a configuration mismatch, not a tunnel limitation.
The fix (start here)
Terminal 1 — Vite dev server:
npm run dev
Terminal 2 — tunnel:
npx localtolink --port 5173
Update vite.config.ts:
import { defineConfig } from "vite";
export default defineConfig({
server: {
hmr: {
clientPort: 443,
protocol: "wss",
},
allowedHosts: [".localtolink.com"],
},
});
Open the tunnel HTTPS URL. Edit a component. HMR should reconnect through the secure WebSocket path.
Why HMR breaks without config
Vite's dev server serves JavaScript over HTTP and opens a WebSocket for hot updates. On plain localhost, the browser connects to ws://localhost:5173.
Through a tunnel, the browser loads the page from https://YOUR-SUB.localtolink.com. Vite's client tries to open a WebSocket to the wrong host/port unless you tell it:
- Use WSS (secure WebSocket) because the page is HTTPS
- Connect on port 443 (where the browser sees the public URL)
- Allow the tunnel hostname in allowedHosts (Vite 6+ host check)
LocalToLink forwards WebSocket traffic when the client connects correctly — the tunnel is not blocking HMR; the client was pointing at localhost.
Step-by-step verification
- Start Vite and confirm HMR on
http://localhost:5173 - Apply the config above and restart Vite
- Start
npx localtolink --port 5173 - Open the HTTPS tunnel URL in a browser tab
- Change a visible string in a component and save
- Watch the browser — update should apply without full page reload
If step 6 fails, open DevTools → Network → WS filter. You should see a WebSocket connection to your tunnel domain. Status 101 Switching Protocols means success.
allowedHosts in Vite 6+
Recent Vite versions validate the Host header against an allowlist. Tunnel domains are not localhost, so Vite may reject requests with:
Blocked request. This host is not allowed.
Adding .localtolink.com (leading dot allows subdomains) fixes browser access through the tunnel. If you use a different tunnel provider, substitute their domain pattern.
For local-only development without a tunnel, you can omit allowedHosts or include localhost.
Mobile testing through the tunnel
LocalToLink prints a QR code in the terminal. Scan it on your phone to load the Vite app over HTTPS — useful for responsive layout checks with real touch events.
HMR from your laptop still pushes updates to the phone browser as long as the phone tab stays open on the tunnel URL. Expect slightly higher latency than localhost; that is normal for any tunnel hop.
See test localhost on your phone for QR workflow details.
Common errors and fixes
| Symptom | Fix |
|---|---|
| WebSocket failed | Set hmr.clientPort: 443 and protocol: "wss" |
| Blocked host | Add tunnel domain to allowedHosts |
| Full page reload instead of HMR | Check Vite version; ensure @vitejs/plugin-react is current |
| Works on desktop, not phone | Phone must use HTTPS tunnel URL, not LAN IP |
| 502 on tunnel | Vite not running or wrong --port |
For broader tunnel diagnosis, see tunnel troubleshooting.
Docker + Vite + tunnel
If Vite runs in Docker:
docker run -p 5173:5173 -v $(pwd):/app -w /app node:22 npm run dev
npx localtolink --port 5173
The tunnel targets host port 5173. Vite config is the same. Ensure server.host: true or 0.0.0.0 inside the container so the port binding is reachable from the host.
Next.js note
Next.js Fast Refresh uses different configuration (allowedDevOrigins in Next.js 15.2+). If your stack is Next.js rather than Vite, see Next.js dev server through a tunnel instead of copying Vite settings.
Honest limits
LocalToLink free tier: 2-hour sessions, random subdomain, 1 GB bandwidth, 10 req/s. Sufficient for HMR development and mobile layout checks. Not for load testing or production hosting.
Restarting the tunnel assigns a new subdomain — update any bookmarked URLs. Brief disconnects within 10 minutes may reconnect to the same URL.
FAQ
Does every tunnel need this Vite config?
Any HTTPS tunnel that is not localhost requires HMR client settings pointing at WSS/443. The exact domain goes in allowedHosts.
Will HMR work for a client viewing my tunnel URL?
Yes, while your dev server and tunnel run. The client sees updates when you save files locally.
Can I disable HMR and use full reload?
Yes, but fixing config is usually faster. Set server.hmr: false only if you intentionally want manual refreshes.
Next steps
- Vite HMR through a tunnel — full guide with framework versions
- WebSocket through a tunnel — protocol details
- Expose localhost in one command — tunnel basics
- CLI reference — flags and session limits
Try it now
Start a tunnel from your project directory — no account required.
$ npx localtolink