Skip to content
KCIndex
All notes

Infrastructure /

Why Cloudflare Tunnel Is My Default Tool for Development Tunnels

A quick, secure public URL for the development work that cannot stay on localhost.

7 min read

A small silver ball travelling up a floating red and blue staircase.

I have tried a lot of ways to make a local API or site reachable from the internet: opening a port on a VPS, reverse proxies, SSH reverse tunnels, ngrok, Tailscale Funnel, and the various “run one command and get a URL” services.

I need a tunnel for development work that cannot stay on localhost: an OAuth provider needs a callback URL, Stripe needs to deliver a test event, or a client needs a link before the site is ready to deploy. I also want a stable hostname for the cases where a provider will not accept a changing URL, plus enough access control that I can share a work-in-progress without sharing everything.

I keep coming back to Cloudflare Tunnel. A random public URL is easy to get. Cloudflare gives me that, then gives me a stable development URL and route-level authentication when I need them.

What I want from a tunnel

Most tunnel comparisons start and end with the command that creates a public URL. That is useful, but it is not my main test.

For a development tunnel, I want five things:

  1. No public origin. I do not want to open an inbound firewall port or make a home IP, a laptop, or a small server directly addressable.
  2. A real hostname. dev.example.com should keep working when I restart a machine or move the service.
  3. A stable option when I need one. OAuth dashboards and webhook providers are much easier to configure when the callback URL does not change every morning.
  4. Route-level access control. I want to protect a client preview or landing page while leaving the webhook endpoint reachable.
  5. Low setup overhead. The tunnel should take one command for a test and not require opening a port on my router or server.

Cloudflare Tunnel covers those boxes. cloudflared runs beside the API and creates outbound connections to Cloudflare. Requests reach Cloudflare first, then travel back through that established connection to the local service. I do not need to open an inbound port or publish the origin IP. Cloudflare’s Tunnel documentation describes the model in more detail.

That is enough for a local service to behave like a real internet-facing endpoint without making my development machine publicly addressable.

Why Cloudflare Tunnel won me over

It removes the awkward network work

The old approach is familiar: allocate a public IP, open port 443, terminate TLS somewhere, harden the server, and make sure the proxy remains healthy. It is all manageable, but it is a surprising amount of surface area for a small API.

With a tunnel, the server initiates the connection out. That works well from a VPS, a home lab, a private network, or a machine behind carrier-grade NAT. I can keep inbound traffic closed and still give the API a proper public hostname.

The consequence is practical: moving the API does not require changing the public endpoint. Run the connector near the new service, point the tunnel at it, and the hostname stays the same.

It starts as a throwaway URL and can become a stable development URL

For a quick webhook test, this is enough:

cloudflared tunnel --url http://localhost:3000

Cloudflare gives the process a temporary trycloudflare.com URL. It is excellent for a demo, a browser test, or checking whether a third-party webhook can reach a local endpoint.

Quick Tunnels are temporary, have limitations, and do not come with an uptime guarantee. For a callback URL I will revisit throughout a project, I create a named tunnel and attach dev.example.com. The setup takes a little longer, but I can put the same URL in Stripe or an OAuth dashboard and leave it there.

It gives a development site a proper front door

Once the hostname sits on Cloudflare, I can use TLS, DNS, firewall rules, and Cloudflare Access for a client preview or development page that should stay private.

I do not turn on every Cloudflare product. A public webhook endpoint may only need HTTPS and a rate-limit rule. A client preview can sit behind Access. I can add either without changing the local service.

It is less fragile than an SSH session

For development, I care more about getting a link that works during a demo than building a high-availability architecture. cloudflared maintains multiple outbound connections to Cloudflare data centers, which is a better starting point than an SSH session that disappears when a laptop sleeps. Cloudflare documents the connection model in its tunnel availability guide.

How I expose a local API or site

Here is the setup I use when a local API or site listens on http://localhost:3000.

1. Prove the API works locally

First, make sure the service responds before adding networking complexity:

curl http://localhost:3000/health

For a one-off test, start a Quick Tunnel:

cloudflared tunnel --url http://localhost:3000

Use the printed URL only for development. It is ideal for receiving a test Stripe, GitHub, or OAuth callback while the app is still on a laptop.

2. Create a named tunnel when the development URL needs to stay put

Authenticate cloudflared, then create a tunnel with a descriptive name:

cloudflared tunnel login
cloudflared tunnel create api-production

Create a configuration file such as ~/.cloudflared/config.yml using the UUID and credentials file generated by the second command:

tunnel: YOUR-TUNNEL-UUID
credentials-file: /Users/you/.cloudflared/YOUR-TUNNEL-UUID.json

ingress:
  - hostname: dev.example.com
    service: http://localhost:3000
  - service: http_status:404

The final catch-all rule is intentional. It prevents unexpected hostnames or paths from falling through to a local service.

3. Route a stable development hostname and run it

Create the DNS route and start the connector:

cloudflared tunnel route dns api-production dev.example.com
cloudflared tunnel run api-production

Now https://dev.example.com reaches the local API through the tunnel. I use that URL for OAuth redirect URIs, Stripe webhook endpoints, and client preview links. For a local-only workflow, I run cloudflared beside the development server and stop it when I am done.

A stable development domain saves repeated OAuth setup

This is the part I think is underrated. OAuth providers usually require an exact redirect URI, and browser-based integrations often require an exact authorized JavaScript origin too. A provider dashboard might contain both of these:

Authorized JavaScript origin: https://dev.example.com
Redirect URI:                https://dev.example.com/auth/google/callback

With a Quick Tunnel or any other changing URL, I have to edit those values every time I start a new tunnel. That is annoying on its own. It also creates a predictable failure mode: the OAuth flow works locally, someone restarts the tunnel, and the next login dies with a redirect-URI mismatch.

dev.example.com removes that churn. I register the origin and redirect path once, then point the named tunnel at whichever local server is running today. The same applies to Stripe: I can keep its test webhook endpoint set to https://dev.example.com/api/webhooks/stripe instead of pasting a new address into the dashboard after every restart.

ngrok now includes an assigned development domain on its free plan, so a stable URL does not always require a paid account. But bringing my own dev.example.com to ngrok currently requires its pay-as-you-go plan, listed at $20 per month plus usage. Cloudflare Tunnel is available on all Cloudflare plans, so if I already own the domain and use Cloudflare DNS, I do not pay a separate Tunnel fee. ngrok’s pricing and Cloudflare’s Tunnel documentation are worth checking because plan details change.

At a minimum, I add a health endpoint and make sure application logs include request IDs and the upstream IP headers I trust. Then I choose controls based on exposure:

  • For a client preview: put the hostname or a specific route behind Cloudflare Access.
  • For a webhook receiver: validate the sender’s signature in the application; a tunnel does not replace webhook verification.
  • For an API I am testing from another machine: add a rate limit and keep application authentication turned on.

The tunnel hides the origin path. It does not make an unauthenticated application secure by itself.

Protect the landing page without blocking webhooks

This is one of my favourite parts of the setup. An API is rarely only an API. It may have a landing page, a preview UI, documentation, or a small admin panel that only I or my team should see. At the same time, a service such as Stripe, GitHub, or an OAuth provider needs to call a webhook endpoint without being redirected to a login screen.

Cloudflare Access lets me make that distinction at the edge. It acts as an identity-aware proxy before a request travels through the tunnel, and its self-hosted applications can be scoped to a hostname or a path. More-specific paths take precedence over broader ones, as described in Cloudflare’s application-path documentation.

For example, imagine this API:

https://dev.example.com/                  # landing page — require sign-in
https://dev.example.com/dashboard         # require sign-in
https://dev.example.com/api/webhooks/*    # public — third parties must reach it

I create two Cloudflare Access applications in Zero Trust → Access controls → Applications.

1. Create the protected application

Choose Add an application → Self-hosted, then configure:

Setting Value
Application domain example.com
Subdomain dev
Path Leave blank to cover the whole hostname
Policy action Allow
Include rule My email address, an email domain, or an identity-provider group

Cloudflare will send a visitor who is not already authenticated through the identity provider login flow. Only users matching the Allow policy will reach the landing page and dashboard.

2. Add a narrow webhook exception

Create a second Self-hosted application for the same hostname, but set the path to:

/api/webhooks/*

Attach a policy with:

Setting Value
Policy action Bypass
Include rule Everyone

The webhook application is more specific than the hostname-wide application, so requests to /api/webhooks/* bypass the interactive Access login while the rest of dev.example.com remains protected. Cloudflare documents this exact public-endpoint pattern for webhook and OAuth callback paths in its common Access policies guide.

Bypass makes this endpoint public. It also disables Access controls and Access request logging for matching traffic. I keep the exception narrow and verify the webhook provider’s signature or shared secret inside my application. A public URL does not prove that a request came from Stripe, GitHub, or any other provider.

If a machine I control needs to call a protected route, I do not use Bypass. I use an Access Service Auth policy with a service token instead. That keeps machine-to-machine authentication and avoids teaching an automated client to complete a browser login.

The alternatives I tried and where they fit

The alternatives are good tools that optimize for different jobs.

Option Where it shines Why I prefer Cloudflare Tunnel for development
ngrok The fastest polished developer experience, especially for inspecting and replaying HTTP requests. I want an optional stable development domain, an invisible origin, and Access controls in the same place.
SSH reverse tunnel Debugging a server or escaping a restrictive network with tools already installed. It needs a public relay server and careful supervision. It is a useful emergency tool, not the API ingress architecture I want to maintain.
Tailscale Funnel Safely exposing something already inside a Tailscale network. Tailscale is excellent for private mesh networking. For a public development URL on a custom domain, Cloudflare’s DNS and Access workflow are a closer fit.
Port forwarding + Nginx/Caddy Full control and a straightforward conventional server setup. It makes the origin public and asks me to maintain a reverse proxy for a development task.
A preview deployment Sharing a deployable site with a client. I use one when it exists. A tunnel is faster when the work is only running locally or needs local services and secrets.

The one alternative I still keep nearby is ngrok. Its request inspector is genuinely great when I am debugging a webhook payload and want to see exactly what arrived. For the OAuth callback, Stripe endpoint, or client-preview link I will reuse all week, I reach for Cloudflare Tunnel.

The trade-offs worth admitting

Cloudflare Tunnel has a few real costs.

First, it is a Cloudflare-shaped workflow. To use a custom development domain, the DNS zone lives on Cloudflare. That is useful when I want the wider platform, but it is vendor coupling.

Second, it does not replace application security. Even a development API needs authentication, authorization, input validation, logging, and webhook signature verification.

Finally, I do not use it for every local problem. I use a Quick Tunnel for a five-minute share. If two teammates need private access, I may use Tailscale. I use the named setup when a callback or preview URL needs to be stable.

Why I keep choosing it

Cloudflare Tunnel starts with a simple command, keeps my local machine off the public internet, and supports a stable development hostname when I need one. That makes it a good fit for the work I do before a proper deployment exists: OAuth callbacks, Stripe webhooks, and client feedback.

Read next

I Ditch Desktop AI Apps to Get More from My Coding-Agent Subscriptions

Continue reading