Exposing a Local Web App With a Cloudflare Tunnel
When you want to open a local web app from a phone, tablet, teammate's machine, or temporary external device, a Cloudflare Tunnel is a simple way to expose it without changing router settings. It gives you a public HTTPS URL that forwards traffic back to a local development server.
No port forwarding, no public IP, no DNS setup, and no inbound firewall exposure are required for the quick version.
What We Are Building
A common local full-stack app has two processes:
- Frontend: Vite on
http://127.0.0.1:5173 - Backend API: for example ASP.NET Core, Node, Python, or another API on
http://127.0.0.1:5069
The external browser only talks to the public Cloudflare URL. Cloudflare forwards that traffic to Vite, and Vite proxies backend calls to the local API:
External browser
-> https://random-name.trycloudflare.com
-> Cloudflare edge
-> cloudflared on this machine
-> Vite dev server on 127.0.0.1:5173
-> /api and optional WebSocket routes proxied to the backend on 127.0.0.1:5069
For short-lived demos this is ideal. The URL is random, disposable, HTTPS-enabled, and does not require a Cloudflare account.
Why Cloudflare Tunnel
The normal LAN setup works if your phone and laptop are on the same WiFi, but it can fail because of hotel networks, mobile hotspots, VPNs, firewall rules, or router isolation.
A tunnel avoids that by making an outbound connection from your machine to Cloudflare. Your laptop does not need to accept inbound internet traffic directly.
Cloudflare calls this short-lived mode a Quick Tunnel or TryCloudflare tunnel. The command looks like this:
cloudflared tunnel --url http://localhost:5173
Cloudflare then prints a URL like:
https://some-random-words.trycloudflare.com
That is the URL you open from the external device.
Step 1: Install cloudflared
Download cloudflared from Cloudflare:
On Windows, you can put cloudflared.exe somewhere simple, for example:
C:\Tools\cloudflared\cloudflared.exe
Then either add that folder to PATH, or run it with the full path:
C:\Tools\cloudflared\cloudflared.exe --version
If it prints a version, the binary is ready.
Step 2: Start the Backend API
Start whatever backend your app uses. For example, a .NET API might look like this:
cd C:\repo\my-app
dotnet run --project src\MyApp.Api\MyApp.Api.csproj --launch-profile http
Or a Node API might look like this:
cd C:\repo\my-app\backend
npm run dev
Check the backend health endpoint if you have one:
Invoke-WebRequest -UseBasicParsing -Uri http://127.0.0.1:5069/api/health
You want HTTP 200.
Step 3: Start the Frontend
In another terminal:
cd C:\repo\my-app\frontend
npm run dev:lan
For a Vite app, dev:lan typically runs Vite with:
vite --host 0.0.0.0
That makes the dev server reachable beyond only localhost. The Cloudflare tunnel can still point at 127.0.0.1, but LAN binding also keeps the same command useful for direct local-network testing.
Check the frontend locally:
Invoke-WebRequest -UseBasicParsing -Uri http://127.0.0.1:5173
Again, you want HTTP 200.
Step 4: Start the Cloudflare Tunnel
In a third terminal:
cloudflared tunnel --url http://127.0.0.1:5173
If cloudflared is not on PATH, use the full path:
C:\Tools\cloudflared\cloudflared.exe tunnel --url http://127.0.0.1:5173
After a few seconds, cloudflared prints a public URL ending in:
.trycloudflare.com
Open that URL from the external device.
Why We Tunnel the Frontend, Not the Backend
For a React/Vite app backed by an API, expose Vite on port 5173, not the backend API directly.
That matters because the browser app loads frontend assets from Vite, and Vite can proxy backend traffic:
/apigoes to the backend API/hubs,/ws, or another WebSocket route goes to the backend when needed
So from the external browser's point of view, everything appears to come from the same public origin:
https://random-name.trycloudflare.com
That avoids mixed-origin problems for normal API calls and WebSockets.
If you only need to expose an API without a frontend, point the tunnel directly at the API instead:
cloudflared tunnel --url http://127.0.0.1:5069
Vite Proxy Requirement
For a frontend + backend setup, the frontend dev server should proxy API and WebSocket requests to the backend. In a Vite app, this lives in vite.config.ts.
Conceptually it needs this:
server: {
proxy: {
'/api': 'http://127.0.0.1:5069',
'/hubs': {
target: 'http://127.0.0.1:5069',
ws: true,
},
},
}
The important bit is ws: true for any WebSocket route. For example, SignalR often uses /hubs, while other apps might use /ws, /socket, or /socket.io.
Backend CORS
In this setup, CORS is less painful because the external browser calls the Cloudflare URL, and Vite proxies backend calls server-side during development.
Still, your backend CORS policy should support the local dev origins used by Vite:
http://localhost:5173
http://127.0.0.1:5173
For direct LAN testing, you may also want to allow private network origins on port 5173, such as:
http://192.168.x.x:5173
http://10.x.x.x:5173
http://172.16.x.x:5173
That is separate from the Cloudflare tunnel, but useful when testing directly on WiFi.
Quick Tunnel vs Named Tunnel
The command above creates a Quick Tunnel:
cloudflared tunnel --url http://127.0.0.1:5173
That is best for:
- A temporary demo
- Sharing a local app briefly
- Testing from a phone or tablet
- Avoiding Cloudflare account setup
The tradeoffs:
- The URL is random.
- The URL changes when you restart the tunnel.
- It is not meant to be permanent infrastructure.
- Anyone with the URL can try to access it.
For a longer-running setup, use a named tunnel with a Cloudflare account and your own domain. That gives you stable DNS, access policies, service installation, and better operational control.
Security Notes
Treat the random URL as public.
It is obscure, but not authentication. For a quick demo that may be enough. For anything long-lived, add one of:
- Cloudflare Access
- App-level authentication
- A private token in the app
- A named tunnel behind your own domain
Also remember that the local app is still your development instance. If it has admin endpoints, debug tools, seed data, or destructive buttons, the tunnel exposes those too.
As a rule of thumb, a Quick Tunnel is fine for short private checks, but it should not be left running indefinitely without authentication.
Troubleshooting
If the Cloudflare URL opens but the page is blank:
- Confirm Vite is running on
5173. - Open
http://127.0.0.1:5173locally. - Check the Vite terminal for errors.
If API data does not load:
- Confirm the backend is running on
5069. - Open
http://127.0.0.1:5069/api/health. - Check that Vite is proxying
/apito the backend.
If WebSockets do not connect:
- Confirm your WebSocket route is proxied with
ws: true. - Check browser dev tools for WebSocket errors.
- Restart Vite and
cloudflared.
If the tunnel command starts but no URL appears:
- Check your internet connection.
- Try a newer
cloudflaredbinary. - Make sure no local Cloudflare config is interfering with Quick Tunnels.
If the URL stops working:
- Check that the
cloudflaredprocess is still running. - Quick Tunnel URLs are tied to that running process.
- Restarting the process normally creates a new URL.
Useful Commands
Backend:
cd C:\repo\my-app
dotnet run --project src\MyApp.Api\MyApp.Api.csproj --launch-profile http
Frontend:
cd C:\repo\my-app\frontend
npm run dev:lan
Tunnel:
cloudflared tunnel --url http://127.0.0.1:5173
Health checks:
Invoke-WebRequest -UseBasicParsing -Uri http://127.0.0.1:5069/api/health
Invoke-WebRequest -UseBasicParsing -Uri http://127.0.0.1:5173
References
- Cloudflare Quick Tunnels: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/
- Cloudflare
cloudflareddownloads: https://developers.cloudflare.com/tunnel/downloads/ - Cloudflare Tunnel configuration: https://developers.cloudflare.com/tunnel/configuration/

Comments
Post a Comment