Give Your Local Dev Server a Stable Public HTTPS URL with Cloudflare Tunnel
Use a named Cloudflare Tunnel to expose localhost over HTTPS on your own domain — no port forwarding, no firewall rules, no certificate wrangling.
What You'll Build
A named Cloudflare Tunnel that exposes a local HTTP server at a persistent public HTTPS subdomain (e.g., dev.example.com) — no port forwarding, no firewall changes, and TLS provisioned automatically by Cloudflare.
Prerequisites
- A domain managed in Cloudflare (nameservers delegated to Cloudflare — free plan works)
- A local HTTP server running on port 3000 (adjust the port in config for anything else)
- macOS 12+, Debian/Ubuntu, or Windows 10/11; commands shown for macOS/Linux
cloudflaredCLI — installed in Step 1
Step 1 — Install cloudflared
macOS (Homebrew):
brew install cloudflared
Debian / Ubuntu (amd64):
curl -L -o cloudflared.deb \
https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared.deb
On ARM hosts (Raspberry Pi, Ampere), use
cloudflared-linux-arm64.debinstead.
Windows:
winget install Cloudflare.cloudflared
Confirm the install:
cloudflared --version
Step 2 — Authenticate cloudflared
cloudflared tunnel login
A browser window opens. Select your Cloudflare account and the zone (domain) you want to use. cloudflared saves an origin certificate to ~/.cloudflared/cert.pem and closes the prompt automatically.
Step 3 — Create the Named Tunnel
cloudflared tunnel create my-dev-tunnel
cloudflared prints a UUID — something like a1b2c3d4-5678-... — and writes credentials to ~/.cloudflared/<UUID>.json. Copy the UUID now; you need it in Step 5.
Verify it was created:
cloudflared tunnel list
Step 4 — Route a DNS Record to the Tunnel
Replace dev.example.com with the subdomain you want:
cloudflared tunnel route dns my-dev-tunnel dev.example.com
This adds a CNAME in your Cloudflare zone resolving dev.example.com → <UUID>.cfargotunnel.com. No IP address or A record is needed.
Step 5 — Write the Config File
Create ~/.cloudflared/config.yml. Substitute the real UUID from Step 3 and the correct credentials path for your OS.
Linux:
tunnel: <UUID>
credentials-file: /home/<your-username>/.cloudflared/<UUID>.json
ingress:
- hostname: dev.example.com
service: http://localhost:3000
- service: http_status:404
macOS: replace /home/<your-username> with /Users/<your-username>.
Windows: use forward slashes to avoid YAML escaping issues:
tunnel: <UUID>
credentials-file: C:/Users/<your-username>/.cloudflared/<UUID>.json
ingress:
- hostname: dev.example.com
service: http://localhost:3000
- service: http_status:404
Three things to get right:
| Field | Notes |
|---|---|
tunnel |
The UUID from Step 3 — the tunnel name also works, but UUID is unambiguous |
credentials-file |
Full absolute path; cloudflared does not reliably expand ~/ here |
| Catch-all rule | The final service: http_status:404 entry is required — cloudflared refuses to start without it |
Your local server stays plain HTTP on localhost:3000. Cloudflare handles the public TLS certificate automatically.
Validate the ingress rules before running:
cloudflared tunnel ingress validate
Step 6 — Run the Tunnel
cloudflared tunnel run
Do not pass the tunnel name or UUID as an argument. Running
cloudflared tunnel run my-dev-tunnelcauses cloudflared to bypass your config file's ingress rules — traffic never reacheslocalhost:3000and callers see a 502 Bad Gateway. With no argument, cloudflared reads thetunnel:UUID and allingress:rules from~/.cloudflared/config.ymlautomatically.
Healthy startup looks like:
INF Starting tunnel tunnelID=<UUID>
INF Connection registered connIndex=0 location=SJC
INF Connection registered connIndex=1 location=SEA
INF Connection registered connIndex=2 location=LAX
INF Connection registered connIndex=3 location=SJC
Four connections to Cloudflare's edge are expected and normal.
Verify It Works
Open https://dev.example.com in a browser — you should reach your local app.
Or from the terminal:
curl -I https://dev.example.com
Expected: HTTP/2 200 (or your app's actual status code). TLS is provisioned automatically by Cloudflare — no Certbot or Let's Encrypt setup required.
Check live tunnel status:
cloudflared tunnel list
Troubleshooting
502 Bad Gateway at the public URL
Your local server isn't running or is on a different port. Test with curl http://localhost:3000. Also confirm you ran cloudflared tunnel run with no name argument (see Step 6).
No config file found or ingress rules silently ignored
cloudflared couldn't locate ~/.cloudflared/config.yml. Confirm the file exists at that exact path and contains valid YAML. Run cloudflared tunnel ingress validate to surface syntax errors.
Authentication / credential errors at startup
The path in credentials-file is wrong, or cert.pem is stale. Re-run cloudflared tunnel login and verify the absolute path in credentials-file points to a real file.
DNS not resolving
Run dig dev.example.com to confirm the CNAME is live. Cloudflare usually propagates within 30 seconds, but local cache can delay it — test with dig @1.1.1.1 dev.example.com to bypass local resolvers.
Next Steps
- Run at boot:
sudo cloudflared service installregisters the tunnel as a systemd (Linux) or launchd (macOS) service so it starts automatically. - Expose multiple local services: Add more
hostname/servicepairs to theingresslist — one tunnel can serve many subdomains. - Require authentication: Gate the URL with a login page via Cloudflare Zero Trust → Access → Applications.
- Deeper reference: Cloudflare Tunnel documentation
Emeka has spent over a decade tracking threat actors, vulnerability disclosures, and the evolving landscape of application security, bringing a sharp continent-spanning perspective to his reporting. He's known for translating dense CVE advisories into clear, actionable context that developers and security teams alike actually read.
Discussion 0
No comments yet
Be the first to weigh in.