Cloudflare Tunnel as a Free ngrok Alternative for Webhooks
Cloudflare Tunnel — Free ngrok Alternative for Webhooks
I was implementing payments with Stripe and Lemon Squeezy, and needed to test webhooks locally. I've been using ngrok for years, but the free tier gives you a random URL every time you restart. That means updating your webhook URL in every dashboard, every time. And some of my older projects are still pointing to the same ngrok domain — I can't just change it.
I looked into alternatives. LocalCan looked really compelling but it's $89. playit.gg exists too. But then I realized — if you already have a domain on Cloudflare, you can do this for free with cloudflared. It's not exactly advertised for this use case, but it works perfectly.
Set it up once, run it when you need it, kill it when you don't.
Prerequisites
- A domain already on Cloudflare (DNS managed by Cloudflare)
cloudflaredinstalled:
# macOS
brew install cloudflared
# Windows
winget install --id Cloudflare.cloudflared
# Linux
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o cloudflared
chmod +x cloudflared && sudo mv cloudflared /usr/local/binOne-Time Setup
1. Login
cloudflared tunnel loginThis opens your browser — select your domain and approve.
2. Create a tunnel
cloudflared tunnel create my-dev-tunnelCopy the UUID from the output (looks like aaafc451-1eea-47a4-81e2-3c02b3907c37). You can always get it later with cloudflared tunnel list.
3. Create the config file
Create ~/.cloudflared/config.yaml:
tunnel: <YOUR-TUNNEL-UUID>
credentials-file: /Users/yourname/.cloudflared/<YOUR-TUNNEL-UUID>.json
ingress:
- hostname: webhook.yourdomain.com # your subdomain
service: http://localhost:3000 # your local dev server port
- service: http_status:404Only two things to change here: the subdomain and the port. Everything else is boilerplate.
4. Point your subdomain to the tunnel
cloudflared tunnel route dns my-dev-tunnel webhook.yourdomain.comThis auto-creates a CNAME in your Cloudflare DNS. No dashboard clicking needed.
Daily Usage
# Start
cloudflared tunnel run my-dev-tunnel
# Stop
Ctrl+CThat's it. Your webhook URL is always https://webhook.yourdomain.com. Set it once in Stripe/Lemon Squeezy/whatever and never touch it again.
Why not just use ngrok?
| Cloudflare Tunnel | ngrok (free) | Other solutions (e.g., untun) | |
|---|---|---|---|
| Persistent URL | ✅ Always the same | ❌ Available, but can't ever change | ❌ Changes every restart |
| Custom domain | ✅ Free | ❌ Paid only | ⚠️ Varies |
| Bandwidth limits | ❌ None | ✅ Yes | ⚠️ Varies by provider |
| Session timeouts | ❌ None | ✅ Yes | ⚠️ Varies by provider |
| Request inspector UI | ❌ No | ✅ Yes | ⚠️ Varies |
| Cost | Free | Free / $10+/mo | Free / Varies / Expensive |
The tradeoff is you lose ngrok's request inspector, but honestly I just use the Stripe CLI or check my server logs for that.
Notes
- The tunnel only runs while the process is running — no background daemon unless you explicitly set one up.
- All connections are outbound-only from your machine to Cloudflare's edge — no open inbound ports.
- Config file lives at
~/.cloudflared/config.yamlon macOS/Linux,%USERPROFILE%\.cloudflared\config.yamlon Windows.