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

TunnelsService Bindings
ConnectsCF edge to non-Worker originWorker to Worker
Requirescloudflared daemon on originNothing (CF-internal)
LatencyNetwork round-trip to originZero (in-process call)
Use whenTarget is a local/private serverTarget is another Worker
AuthCloudflare Access, origin certsAutomatic (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.