I needed a way to expose local dev servers to the internet for webhook testing. ngrok works, but Cloudflare Tunnel is free and lets you use your own domain whereas ngrok is free, but you pay $10 (as of 20251201) per custom domain.

Prerequisite: Cloudflare must already be managing DNS for your domain.

Install and Login

brew install cloudflared
cloudflared tunnel login

The login command opens your browser to authenticate with Cloudflare.

Create a Tunnel

You only need one tunnel. You can route multiple subdomains through the same tunnel. I’ll use dev as the tunnel name in these examples:

cloudflared tunnel create dev

This creates the tunnel and stores credentials at ~/.cloudflared/<tunnel-id>.json.

Create DNS Routes

This creates CNAME records pointing your subdomains to the tunnel:

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

Tip: Keep it simple—use subdomains under the same root domain even for different projects. Instead of dev.project1.com and dev.project2.com, use dev.yourdomain.com and dev2.yourdomain.com. Otherwise, you’d find the cloudflared tunnel route dns <name> <subdomain> command creating CNAME records in your first domain. It’s apparently not supported unless you use the web dashboard to manage your tunnels. It’s much simpler just to stick to 1 root domain.

To remove a route, delete the CNAME record from the Cloudflare dashboard.

Configure and Run

Create ~/.cloudflared/config.yml:

tunnel: <tunnel-id>
credentials-file: /Users/you/.cloudflared/<tunnel-id>.json

ingress:
  - hostname: dev.yourdomain.com
    service: http://localhost:5174
  - hostname: dev-backend.yourdomain.com
    service: http://localhost:4002
  - service: http_status:404

The last rule is a required catch-all for unmatched requests.

Then normally, you’d just run:

cloudflared tunnel run dev

Both subdomains now route to their respective local ports.