Tunnels
Expose a local development server to the internet through a Cloudflare Tunnel. The Webhook Hub uses this to forward webhooks to services running on your machine without opening firewall ports or configuring NAT.
Prerequisites: Networking Essentials, First Worker
What Tunnels Are (and Aren’t)
Cloudflare Tunnels connect the Cloudflare edge to a non-Worker origin - your laptop, a VM, a Raspberry Pi, or a private server. The cloudflared daemon runs on the origin and maintains an outbound connection to Cloudflare. Traffic flows through that tunnel without exposing any ports.
Important: Tunnels are for connecting to non-Worker origins. For Worker-to-Worker communication, use service bindings instead. Service bindings let one Worker call another directly on Cloudflare’s network with zero network overhead. Don’t use a Tunnel when both ends are Workers.
Install cloudflared
# macOS
brew install 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
# Verify
cloudflared --version
Quick Tunnel (No Config)
The fastest way to expose a local server. No Cloudflare account setup needed:
# Start your local service
node server.js # listening on port 3000
# In another terminal, expose it
cloudflared tunnel --url http://localhost:3000
cloudflared prints a temporary URL like https://random-words.trycloudflare.com. Anyone can reach your local server at that URL. The tunnel stops when you kill the process.
This is useful for quick testing, but the URL changes every time. For persistent routing, create a named tunnel.
Named Tunnel (Persistent)
Authenticate
cloudflared tunnel login
This opens a browser to authorize cloudflared with your Cloudflare account. A certificate is saved to ~/.cloudflared/cert.pem.
Create the Tunnel
cloudflared tunnel create webhook-local
Output:
Created tunnel webhook-local with id a1b2c3d4-...
The tunnel ID and credentials file are stored in ~/.cloudflared/.
Configure Routing
Create ~/.cloudflared/config.yml:
tunnel: a1b2c3d4-your-tunnel-id
credentials-file: /home/you/.cloudflared/a1b2c3d4-your-tunnel-id.json
ingress:
- hostname: webhooks-local.yourdomain.com
service: http://localhost:3000
- hostname: api-local.yourdomain.com
service: http://localhost:8080
# Catch-all (required)
- service: http_status:404
The ingress rules map hostnames to local services. The catch-all at the bottom is required.
Add DNS Route
cloudflared tunnel route dns webhook-local webhooks-local.yourdomain.com
This creates a CNAME record pointing webhooks-local.yourdomain.com to your tunnel.
Run the Tunnel
cloudflared tunnel run webhook-local
Now webhooks-local.yourdomain.com routes through Cloudflare’s edge to localhost:3000 on your machine.
Webhook Hub Use Case
The Webhook Hub receives webhooks at the edge and queues delivery jobs. Some delivery targets are local development servers. Instead of exposing those servers directly, run a Tunnel:
GitHub push event
-> Webhook Hub Worker (edge)
-> Queue consumer delivers to https://webhooks-local.yourdomain.com/hook
-> Cloudflare Tunnel
-> localhost:3000/hook on your machine
Set up a forwarding rule pointing to the tunnel hostname:
curl -X POST https://webhook-hub.your-worker.workers.dev/rules \
-H "Content-Type: application/json" \
-d '{
"source": "github",
"target_url": "https://webhooks-local.yourdomain.com/hook"
}'
Now GitHub webhooks flow through the Hub, get queued, and deliver to your local machine through the Tunnel.
Running as a Service
For always-on tunnels (e.g., on a home server), install cloudflared as a system service:
# Linux (systemd)
sudo cloudflared service install
# macOS (launchd)
sudo cloudflared service install
This registers cloudflared to start on boot using your config.yml.
Multiple Local Services
A single tunnel can route to multiple local services via ingress rules:
ingress:
- hostname: app.yourdomain.com
service: http://localhost:3000
- hostname: api.yourdomain.com
service: http://localhost:8080
- hostname: grafana.yourdomain.com
service: http://localhost:3001
- service: http_status:404
Each hostname needs a DNS route:
cloudflared tunnel route dns webhook-local app.yourdomain.com
cloudflared tunnel route dns webhook-local api.yourdomain.com
cloudflared tunnel route dns webhook-local grafana.yourdomain.com
Tunnels vs Service Bindings
| Tunnels | Service Bindings | |
|---|---|---|
| Connects | CF edge to non-Worker origin | Worker to Worker |
| Requires | cloudflared daemon on origin | Nothing (CF-internal) |
| Latency | Network round-trip to origin | Zero (in-process call) |
| Use when | Target is a local/private server | Target is another Worker |
| Auth | Cloudflare Access, origin certs | Automatic (same account) |
Gotcha: A common mistake is creating a Tunnel to connect two Workers. This adds unnecessary latency and complexity. Use service bindings for Worker-to-Worker calls - they execute as in-process function calls with no network overhead.
What You Built
You can now expose any local service through Cloudflare’s edge without opening ports. The Webhook Hub delivers to local targets through Tunnels, keeping your development machine secure.
Next: Cron Triggers to add scheduled health checks for forwarding targets.