Skip to main content
  1. Blog/

Cloudflare Tunnel: Expose localhost for Webhooks and OAuth

·8 mins
Nitin Kumar Singh
Author
Nitin Kumar Singh
I build enterprise AI solutions and cloud-native systems. I write about architecture patterns, AI agents, Azure, and modern development practices — with full source code.
Table of Contents

Every developer hits the same wall eventually. You are building a webhook integration, testing an OAuth flow that demands a public redirect URI, or trying to show a client a work-in-progress without deploying it. Your app is running fine on localhost:5000, but the outside world cannot reach it.

The usual workarounds all have friction:

  • ngrok free tier: random URL that changes on every restart, session time limits, no custom domains without a paid plan.
  • Port forwarding on your router: requires admin access, exposes your home IP, does not work in most corporate networks or when you are on a hotspot.
  • Dynamic DNS: still needs an open port and a static-ish IP, and usually involves configuring a router you do not own.
  • Deploying to staging: breaks your tight feedback loop. You are now maintaining two environments to test a single webhook.

Cloudflare Tunnel solves all of this. It is free for the core functionality, works behind NAT and firewalls, and gives you a stable subdomain tied to your own domain.

What Cloudflare Tunnel Is
#

Cloudflare Tunnel (previously Argo Tunnel) creates an outbound-only encrypted connection from a daemon running on your machine (cloudflared) to Cloudflare’s edge network. Traffic from the public internet hits Cloudflare’s edge, gets routed through that persistent connection, and arrives at your local service — without any inbound port needing to be open.

graph LR A[Browser / External Client] -->|HTTPS request| B[Cloudflare Edge] B -->|Encrypted tunnel\nQUIC/HTTP2| C[cloudflared daemon\non your machine] C -->|localhost| D[Local Service\nlocalhost:5000] style A fill:#f4a261,stroke:#e76f51,color:#000 style B fill:#2563eb,stroke:#1d4ed8,color:#fff C:::tunnel style D fill:#16a34a,stroke:#15803d,color:#fff classDef tunnel fill:#0e7490,stroke:#155e75,color:#fff

The key point: no inbound firewall rules, no port forwarding, no exposed IP address. The tunnel is initiated from inside your network outward, so it works from a laptop on a hotel WiFi the same as it does from your corporate dev machine behind a proxy.

Prerequisites
#

  • A Cloudflare account (free tier is sufficient for everything in this post)
  • A domain managed by Cloudflare DNS (you need this for persistent named tunnels; the quick-start temporary URL needs no account at all)
  • cloudflared CLI installed

Installing cloudflared
#

macOS (Homebrew)

brew install cloudflare/cloudflare/cloudflared

Linux (Debian/Ubuntu)

curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o cloudflared.deb
sudo dpkg -i cloudflared.deb

Linux (RPM-based)

curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-x86_64.rpm -o cloudflared.rpm
sudo rpm -i cloudflared.rpm

Windows (winget)

winget install --id Cloudflare.cloudflared

Verify the installation:

cloudflared --version

Quick Start: Temporary Tunnel
#

If you just need a public URL right now and do not care about stability or a custom domain, this requires zero setup:

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

cloudflared connects to Cloudflare’s edge and prints a randomly assigned *.trycloudflare.com URL. Share it, test your webhook, done. The URL disappears when you kill the process. No account needed.

This is fine for a quick demo, but the URL changes every time. For anything repeatable — OAuth redirect URIs, GitHub webhook configurations, client access — you want a persistent named tunnel.

Persistent Named Tunnel with a Custom Subdomain
#

Step 1: Authenticate
#

cloudflared tunnel login

This opens a browser window. Select the domain you want to use. cloudflared writes credentials to ~/.cloudflared/cert.pem.

Step 2: Create the Tunnel
#

cloudflared tunnel create my-dev-tunnel

This creates a tunnel with a stable UUID and writes a credentials file to ~/.cloudflared/<UUID>.json. The tunnel ID does not change regardless of restarts or machine reboots.

List your tunnels to confirm:

cloudflared tunnel list

Step 3: Configure DNS
#

Map a subdomain on your Cloudflare-managed domain to the tunnel. This creates a CNAME record pointing to <UUID>.cfargotunnel.com:

cloudflared tunnel route dns my-dev-tunnel dev.yourdomain.com

dev.yourdomain.com is now permanently routed to your tunnel, regardless of whether the tunnel is running. When the tunnel is down, Cloudflare returns a 503 — nothing leaks your local machine’s address.

Step 4: Create the Config File
#

Create ~/.cloudflared/config.yml:

tunnel: my-dev-tunnel
credentials-file: /Users/yourname/.cloudflared/<UUID>.json

ingress:
  - hostname: dev.yourdomain.com
    service: http://localhost:5000
  - service: http_status:404

The last service: http_status:404 is a catch-all required by cloudflared — any hostname not matched by a rule returns 404.

Step 5: Run the Tunnel
#

cloudflared tunnel run my-dev-tunnel

Your local service on port 5000 is now accessible at https://dev.yourdomain.com, with a valid TLS certificate managed by Cloudflare.

Running as a System Service
#

For a tunnel you want running automatically on login or boot:

# macOS / Linux (installs as launchd/systemd service)
sudo cloudflared service install
sudo systemctl enable cloudflared
sudo systemctl start cloudflared

On Windows:

cloudflared service install

Practical Use Cases
#

Webhook Testing (GitHub, Stripe, Twilio)
#

Configure the webhook endpoint in the provider’s dashboard using your stable subdomain:

https://dev.yourdomain.com/api/webhooks/github

Update your local app’s webhook handler. From this point, every push to GitHub triggers your local code directly — no need to manually replay events or maintain a separate staging endpoint.

For Stripe, the flow is identical. Set the webhook URL in the Stripe dashboard, run your local API, and receive real payment events against your development database. This is significantly faster than the Stripe CLI event forwarding approach when you need the full network path tested.

OAuth Redirect URIs (Azure AD, Google, GitHub OAuth)
#

OAuth providers require redirect URIs to be pre-registered and publicly reachable. With a named tunnel:

  1. Register https://dev.yourdomain.com/auth/callback as an allowed redirect URI in Azure AD, Google Cloud Console, or GitHub OAuth app settings.
  2. Point your local app’s OAuth configuration at this callback URL.
  3. Run the tunnel. OAuth flows now complete end-to-end against your local code.

This is particularly useful when validating token claims, testing MSAL flows, or debugging refresh token handling — scenarios where mocking the OAuth response does not give you full confidence.

Sharing a Dev Build with Stakeholders
#

Send the URL directly. The stakeholder opens https://dev.yourdomain.com in their browser and sees your local build running with live data. No staging deployment, no build pipeline, no environment parity issues. When the meeting ends, kill the tunnel.

Testing Mobile Apps Against a Local API
#

Mobile simulators can usually reach localhost, but physical devices on the same WiFi sometimes cannot — and devices on a cellular connection definitely cannot. With a tunnel, point your mobile app’s API base URL at https://dev.yourdomain.com and test against your local backend from any device, anywhere.

Tunneling Multiple Services
#

The config file supports multiple ingress rules, so one cloudflared process can expose multiple local services under different subdomains:

tunnel: my-dev-tunnel
credentials-file: /Users/yourname/.cloudflared/<UUID>.json

ingress:
  - hostname: api.yourdomain.com
    service: http://localhost:5000
  - hostname: app.yourdomain.com
    service: http://localhost:3000
  - hostname: grafana.yourdomain.com
    service: http://localhost:3001
  - service: http_status:404

Each subdomain needs a corresponding DNS route:

cloudflared tunnel route dns my-dev-tunnel api.yourdomain.com
cloudflared tunnel route dns my-dev-tunnel app.yourdomain.com
cloudflared tunnel route dns my-dev-tunnel grafana.yourdomain.com

This is useful when testing a frontend + backend together, or when you want to share both the application and its monitoring dashboard with a colleague.

Gotchas
#

WebSocket support requires explicit config. If your local service uses WebSockets, add originRequest settings in your ingress rule:

ingress:
  - hostname: dev.yourdomain.com
    service: http://localhost:5000
    originRequest:
      noTLSVerify: false
      connectTimeout: 30s

cloudflared supports WebSockets by default, but proxies sometimes strip Upgrade headers. If WebSocket connections drop immediately, check your Cloudflare zone settings — make sure the WebSocket toggle is on under Network settings in the Cloudflare dashboard.

The domain must be on Cloudflare DNS. You cannot use a domain registered elsewhere with a different nameserver setup. The domain must be fully delegated to Cloudflare’s nameservers.

Cloudflare Access can block your tunnel. If your Cloudflare account has Access policies applied to the zone, they will apply to your tunnel traffic too. This is useful for security (you can require Google SSO before anyone reaches your dev environment), but it will break automated webhook delivery from services like GitHub or Stripe since those cannot authenticate through Access. Either exclude the webhook path from Access policies or use a separate subdomain for webhooks.

HTTPS is enforced. Cloudflare terminates TLS at the edge and communicates to cloudflared over an encrypted tunnel. Your local service can run plain HTTP on localhost — Cloudflare handles the public-facing certificate. Do not configure your local app to expect HTTPS on the internal leg.

Free tier bandwidth limits apply. Cloudflare Tunnel itself is free with no session limits or URL randomization for named tunnels. However, Cloudflare’s free zone plan has no documented bandwidth cap for tunnel traffic as of writing, but Cloudflare reserves the right to throttle abuse. For sustained high-volume traffic (e.g., load testing through the tunnel), use a local tool instead.

Credentials files are sensitive. The JSON credentials file in ~/.cloudflared/ grants full control over the tunnel. Do not commit it to source control, do not put it in a shared directory, and rotate it if you suspect it has been exposed (cloudflared tunnel credentials rotate <tunnel-name>).

Cloudflare Tunnel vs ngrok (Free Tier)
#

FeatureCloudflare Tunnel (Free)ngrok (Free)
Custom subdomainYes, on your own domainNo (random URL)
Stable URL across restartsYesNo
Session time limitsNone2-hour session limit
Concurrent tunnelsMultiple (config-based)1
WebSocket supportYesYes
TCP tunnelingYes (with config)No (paid only)
Requires accountOnly for named tunnelsYes
Requires domain on providerYes (for named tunnels)No
TLS terminationCloudflare edgengrok edge
Traffic inspection UINo (use Cloudflare logs)Yes (local dashboard)
Price for stable URLFree$8/month minimum

ngrok’s local traffic inspection dashboard (http://localhost:4040) is genuinely useful for debugging webhook payloads — that is the one feature Cloudflare Tunnel does not replicate. If you are debugging a webhook payload format and want to inspect raw requests without touching your application code, ngrok’s inspector is faster for that specific task. For everything else — stable URLs, custom domains, multiple services, production-grade reliability — Cloudflare Tunnel is the better choice, especially at zero cost.

Summary
#

Cloudflare Tunnel removes the infrastructure friction from local development scenarios that genuinely require a public URL. The quick-start mode (cloudflared tunnel --url) covers ad-hoc needs with no account required. The named tunnel approach gives you a permanent, stable subdomain under your own domain, running as a system service, with no port exposure and no ongoing cost.

For teams that regularly test OAuth flows, receive webhook events, or share in-progress work with external stakeholders, setting up a named tunnel once and leaving it running is worth the ten minutes of initial configuration.

Related

Building Production-Ready Microservices with .NET Aspire: A Complete E-Commerce Demo

·24 mins
If you’ve ever built a microservices architecture, you know the pain points all too well: spending hours setting up PostgreSQL locally, wrestling with Redis configurations, debugging why RabbitMQ won’t connect, managing connection strings across multiple services, and let’s not even talk about implementing distributed tracing manually.

Elevating Code Quality with Custom GitHub Copilot Instructions

·15 mins
In today’s fast-paced development landscape, AI coding assistants have become indispensable tools for developers seeking to maintain high-quality code while meeting demanding deadlines. GitHub Copilot stands at the forefront of this revolution, offering intelligent code suggestions that can significantly accelerate development. However, the true power of Copilot lies not just in its base capabilities, but in how effectively it can be customized to align with your specific project standards and best practices.

GitHub Codespaces: Streamlining Cloud-Based Development

·8 mins
Say goodbye to “It works on my machine” problems forever! GitHub Codespaces provides ready-to-code development environments in the cloud that work exactly the same for everyone on your team. This guide walks through everything you need to know to get started, even if you’ve never used cloud-based development environments before.