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:

  • /api goes 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:5173 locally.
  • 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 /api to 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 cloudflared binary.
  • Make sure no local Cloudflare config is interfering with Quick Tunnels.

If the URL stops working:

  • Check that the cloudflared process 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

Comments

Popular Posts