"I want my Vite / Next / React dev server reachable from my phone — and I want HMR to keep working."
asd expose <port> is enough for the URL. The two things that bite
people: dev servers binding to 127.0.0.1 only, and HMR over
WebSockets needing a tiny bit of configuration. This page covers
both.
Pick the framework you're running. The recipe is the same shape;
only the port and the --host flag differ.
| Framework | Default port | Bind-to-all flag |
|---|---|---|
| Vite | 5173 | --host (or --host=0.0.0.0) |
| Next.js | 3000 | next dev -H 0.0.0.0 |
| Create React App | 3000 | HOST=0.0.0.0 npm start |
| Astro | 4321 | --host |
| Nuxt | 3000 | nuxt dev --host |
| Storybook | 6006 | --host 0.0.0.0 |
Terminal A — dev server bound to all interfaces:
# Vite
npm run dev -- --host
# Next
npx next dev -H 0.0.0.0
# CRA
HOST=0.0.0.0 npm start
Terminal B — expose it:
$ asd expose 5173 my-app
✓ Exposed http://localhost:5173 → https://my-app-xyz1.eu2.tn.example.com
Two terminals, two commands. Open the URL on your phone. HMR /
live-reload works through the tunnel — Caddy handles the WebSocket
upgrade transparently.
A few things to verify if something looks off:
# Is the dev server actually listening on all interfaces?
ss -tlnp | grep :5173
# Want to see: 0.0.0.0:5173 or *:5173
# Bad sign: 127.0.0.1:5173 — the dev server is localhost-only, fix the --host flag
# Is the tunnel up?
asd expose list
# Does HMR connect? In the browser console you should see:
# [vite] connecting...
# [vite] connected.
127.0.0.1 vs 0.0.0.0. Dev servers default to localhost-only
for security. The tunnel daemon connects to your machine over SSH;
Caddy forwards traffic to 127.0.0.1:<port> on your machine. So
the dev server actually only needs to listen on 127.0.0.1 to be
reachable through the tunnel. The --host flags above are only
needed if you want to also reach the dev server directly from
another device on your LAN (without the tunnel). If you're only
ever going through the public URL, you can skip them.
HMR over WebSocket. Frameworks dial back to the dev server on
the URL the browser sees. Caddy proxies WebSocket connections
transparently — no special config. If HMR fails, check the dev
server's HMR config: Vite needs no change; Next sometimes needs
experimental: { allowedDevOrigins } set; CRA needs
WDS_SOCKET_HOST set to the public hostname.
Hosts header. Some dev servers (older Next.js, some Webpack
setups) reject requests whose Host: header doesn't match the
expected dev host. If you see a "Invalid Host header" page, set
the server's allowedHosts to include your tunnel subdomain or
use a wildcard.
learn/02-first-service — same outcome, but as a net.manifest.yaml so the URL stays the same.cookbook/share-via-public-url.learn/06-custom-routing §responseHeaders.