Cloudflare Tunnels for Dev Teams — A Beginner's Guide
When you're building a web app locally, there are moments where localhost isn't enough. OAuth providers need a real callback URL. Stripe needs to send webhooks somewhere. A client wants to preview your work-in-progress. This is where Cloudflare Tunnels come in. Once you set one up, you'll wonder how you lived without it.
This guide covers everything from first principles: what tunnels are, how they work, and how to give every developer on your team their own permanent public URL in under 10 minutes.
What Is a Cloudflare Tunnel?
A Cloudflare Tunnel creates an outbound-only connection from your machine to Cloudflare's edge network. There's no need to open firewall ports, configure NAT, or touch your router. Your local server is simply reachable at a real HTTPS URL. It's permanent.
Your machine (localhost:3000)
↕ encrypted outbound connection
Cloudflare edge (alice.yourdomain.com)
↕
The internet
Two files live on each developer's machine. They never go in the repo:
| File | What it does |
|---|---|
~/.cloudflared/cert.pem | Account-level credential that allows creating/deleting tunnels |
~/.cloudflared/<UUID>.json | Tunnel-specific credential that allows running that one tunnel |
Prerequisites
- A domain managed by Cloudflare (free plan works fine)
- Access to the team's Cloudflare account (ask your account owner to invite you under Manage Account → Members)
- macOS with Homebrew (Linux/Windows instructions are in the Cloudflare docs)
One-Time Setup Per Developer
1. Install cloudflared
brew install cloudflared
Verify it works:
cloudflared --version
2. Authenticate with your Cloudflare account
cloudflared tunnel login
This opens a browser window. Log in to Cloudflare and select the domain your team uses (e.g. yourdomain.com). A cert.pem file is saved at ~/.cloudflared/cert.pem. This is your account credential and stays on your machine.
3. Create your personal tunnel
Pick a name that identifies you, for example alice-appname.
cloudflared tunnel create alice-appname
The output includes a UUID. Copy it, as you'll need it in the next step. A credentials file is saved automatically at ~/.cloudflared/<UUID>.json.
4. Create a permanent DNS subdomain
cloudflared tunnel route dns alice-appname local-alice.yourdomain.com
This creates a CNAME record in Cloudflare DNS that points local-alice.yourdomain.com to your tunnel. It's permanent: do this once and the subdomain is yours.
5. Create your local config file
Your project should include a template at .cloudflared/config.template.yml. Copy it:
cp .cloudflared/config.template.yml .cloudflared/config.yml
Then fill in your values:
tunnel: <YOUR-UUID>
credentials-file: ~/.cloudflared/<YOUR-UUID>.json
ingress:
- hostname: local-alice.yourdomain.com
service: http://localhost:3000
- service: http_status:404
config.yml is gitignored and lives only on your machine.
6. Validate your setup
# Check the ingress rules parse correctly
cloudflared tunnel --config .cloudflared/config.yml ingress validate
# Confirm your tunnel appears in the list
cloudflared tunnel list
Daily Usage
Start these in two separate terminals each time you develop:
# Terminal A — your app
pnpm dev
# Terminal B — the tunnel
pnpm dev:tunnel
Your app is now live at https://local-alice.yourdomain.com with a valid TLS certificate.
Verify it's reachable.
curl -I https://local-alice.yourdomain.com
You should get HTTP/2 200 (or whichever status your app returns for /). You should not see a 503 from Cloudflare.
Registering Your Permanent URL for Webhooks and OAuth
Your subdomain never changes. Register it once and forget about it.
OAuth Callbacks (Google, GitHub, etc.)
Add your tunnel URL as an authorised redirect URI in each provider's developer console:
- Google Cloud Console → Credentials → OAuth Client → Authorised redirect URIs:
https://local-alice.yourdomain.com/auth/callback/google - GitHub → Settings → Developer settings → OAuth Apps → Authorization callback URL:
https://local-alice.yourdomain.com/auth/callback/github
Stripe / Polar Webhooks
In your payment provider's dashboard, add a webhook endpoint:
https://local-alice.yourdomain.com/api/webhooks/stripe
Every developer registers their own subdomain in their own test-mode account. No shared secrets, no collisions.
What Goes in the Repo
| File | Committed? | Purpose |
|---|---|---|
.cloudflared/config.template.yml | ✅ Yes | Placeholder template for new team members |
.cloudflared/config.yml | 🚫 No (gitignored) | Each dev's personal tunnel config |
~/.cloudflared/<UUID>.json | 🚫 Never | Tunnel credentials that stay on your machine |
~/.cloudflared/cert.pem | 🚫 Never | Account credential that stays on your machine |
How the Account Owner Adds a Team Member
- Go to dash.cloudflare.com → Manage Account → Members
- Invite by email with the Cloudflare Tunnel permission (or Administrator for full access)
- The new member runs
cloudflared tunnel login, selects the shared domain, and follows steps 3–6 above
Each person ends up with their own tunnel, their own subdomain, and their own credentials. Nobody shares secrets. Nobody steps on anyone else's tunnel.
Troubleshooting
cloudflared tunnel list fails / cert.pem not found
Run cloudflared tunnel login again to re-authenticate.
pnpm dev:tunnel fails with "credentials file not found"
Re-run cloudflared tunnel create <name>. The .json file was not created or deleted.
Public URL returns 503 Service Unavailable
Check that pnpm dev is running and started on the expected port. Verify which port Nuxt is using and update service: in config.yml accordingly.
Nuxt started on port 3001 (expecting 3000)
Something else is occupying port 3000. Free the port, or update the service: field in .cloudflared/config.yml to match:
service: http://localhost:3001
ERR_TOO_MANY_REDIRECTS on the tunnel URL
This happens when your app forces HTTPS but Cloudflare is sending HTTP to the origin. Cloudflare Tunnels already handle TLS at the edge. Set originServerName or ensure the service: in config.yml uses http://, not https://.
Summary
| Step | Who | Frequency |
|---|---|---|
| Invite team member to Cloudflare account | Account owner | Once per person |
cloudflared tunnel login | Developer | Once (or after cert.pem expires) |
cloudflared tunnel create + route dns | Developer | Once per machine |
Copy config.template.yml → config.yml and fill in values | Developer | Once per machine |
pnpm dev + pnpm dev:tunnel | Developer | Every dev session |
Cloudflare Tunnels solve the localhost problem permanently. There are no ngrok subscriptions, no random URLs that change on restart, and no firewall headaches. A stable HTTPS address on your own domain is ready whenever you are.
