Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/img/avatar/hanoii.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Comment thread
hanoii marked this conversation as resolved.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions src/content/authors/ariel-barreiro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
name: Ariel Barreiro
firstName: Ariel
avatarUrl: /img/avatar/hanoii.jpg
---

Decades building web applications with Drupal; module maintainer and patch contributor - [hanoii](https://drupal.org/u/hanoii). I got to work in very different stacks, platforms and computer areas over the years, including assembler! Whenever I can, I love skiing.
274 changes: 274 additions & 0 deletions src/content/blog/cloudflare-workers-live-requests-on-your-local.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
---
title: "Using Cloudflare Workers to tunnel matching traffic into your local"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
title: "Using Cloudflare Workers to tunnel matching traffic into your local"
title: "Down the Rabbit Hole: Using Cloudflare Workers to tunnel matching traffic into your DDEV project"

pubDate: 2026-04-07
summary: "A Cloudflare Worker that selectively routes your production traffic to a local DDEV tunnel — so you can set breakpoints on requests you can't easily reproduce anywhere else."
author: Ariel Barreiro
featureImage:
src: /img/blog/2026/04/cloudflare-worker-ddev-cover.png
alt: A graphic connecting traffic handled by a Cloudflare Worker sending some traffic to your tunnel.
categories:
- DevOps
---

## Why Would You Ever Do This?
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before "Why would you do this", a quick section saying in the simplest possible terms what "this" is. My suggestion probably isn't great, but might be a starter.

Suggested change
## Why Would You Ever Do This?
## What In the World Is This?
Cloudflare has lots of wonderful available services and capabilities.
I needed to debug one specific page in a real live site, but wanted to debug it on my local DDEV project.
I configured a Cloudflare worker to redirect traffic for that one page/situation to my local project.
## Why Would You Ever Do This?


You probably wouldn't ever. I needed it for a SAML integration I was working on. The IdP was locked to the production URL, and every small tweak meant a full deploy cycle just to see if it worked. I wanted to iterate fast — and for some edge cases with Xdebug — without touching production. So I built a small Cloudflare Worker that quietly routes my browser session to my local DDEV environment while everyone else keeps hitting the real server.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other possible examples for this kind of thing: Payment system callbacks to production site, (others?)


## How It Works

This only works if your domain is already **proxied through Cloudflare** (the orange cloud ☁️ in your DNS settings). When that's the case, every request passes through Cloudflare's edge before reaching your origin, which means you can intercept and reroute it with a Worker.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if there's a way to make ☁️ orange :) But probably a little screenshot would improve this.


The Worker adds a simple opt-in toggle: you visit your production URL with `?cf_local_debug=1`, and the Worker checks that the request comes from your IP. If it does, it sets a short-lived cookie and redirects you back. From that point on, as long as that cookie is present _and_ the request comes from your IP, the Worker transparently proxies all traffic to your local DDEV tunnel instead of the normal origin. Everyone else continues hitting production as if nothing happened.

![Visiting the production URL with ?cf_local_debug=1 routes your browser to local DDEV, ?cf_local_debug=0 restores production](/img/blog/2026/04/cloudflare-worker-ddev-demo.gif)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like it to be clickable, maybe I'll deal with it later in a different PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nothing for me to action ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I opened #616 for this.


Here's the full flow:

1. Your domain is proxied through Cloudflare and the Worker is attached to its routes (`myapp.example.com/*`).
2. You run `ddev share --provider=cloudflared` (or any other share provider) to expose your local environment via a Cloudflare Tunnel URL (e.g. `https://foo-bar.trycloudflare.com`).
3. You set that URL as the `DEBUG_ORIGIN` secret on the Worker, along with your current public IP as `DEBUG_IP`.
4. You visit `https://myapp.example.com/?cf_local_debug=1`.
5. The Worker sets the debug cookie and redirects you to the clean URL.
6. Subsequent requests from your browser go to your local DDEV — Xdebug, local database, local code and all.
7. When you're done, visit `?cf_local_debug=0` to clear the cookie, or let it expire on its own.

## Setting Up the Worker

You'll need a Cloudflare account and the [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/). Create a new folder for the worker and add these three files:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't understand where this folder would be created. An example would be great. How about a demo repo on github?

For now I'm trying a directory named cloudflare-worker at the top of my project.


**`package.json`**

```json
{
"name": "cloudflare-worker-ddev",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"deploy": "wrangler deploy"
},
"devDependencies": {
"wrangler": "^4.6.0"
}
}
```

**`wrangler.jsonc`**

```jsonc
{
"name": "cloudflare-worker-ddev",
"main": "src/index.js",
"compatibility_date": "2026-04-07",
"workers_dev": false,
}
```

Routes are intentionally kept out of `wrangler.jsonc` to avoid hardcoding your domain in source control — you'll add those in the dashboard in a moment.

**`src/index.js`**
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for linking these items to the GitHub repository and mentioning that they might get out of date here.


```js
function parseCookies(cookieHeader) {
const cookies = {}
if (!cookieHeader) return cookies
for (const part of cookieHeader.split(";")) {
const index = part.indexOf("=")
if (index === -1) continue
const key = part.slice(0, index).trim()
const value = part.slice(index + 1).trim()
cookies[key] = value
}
return cookies
}

function buildDebugCookie(name, value, maxAge) {
return [
`${name}=${value}`,
"Path=/",
`Max-Age=${maxAge}`,
"HttpOnly",
"Secure",
"SameSite=None",
].join("; ")
}

function buildRedirectResponse(url, cookieName, cookieValue, maxAge) {
return new Response(null, {
status: 302,
headers: {
Location: url.toString(),
"Set-Cookie": buildDebugCookie(cookieName, cookieValue, maxAge),
"Cache-Control": "private, no-store",
},
})
}

function getConfig(env) {
const debugOrigin = env.DEBUG_ORIGIN
const debugIp = env.DEBUG_IP
const debugCookie = env.DEBUG_COOKIE?.trim() || "cf_local_debug"
const debugMaxAge = env.DEBUG_MAX_AGE ? Number(env.DEBUG_MAX_AGE) : 3600
return { debugOrigin, debugIp, debugCookie, debugMaxAge }
}

export default {
async fetch(request, env) {
const requestUrl = new URL(request.url)

// Images always go to the real origin — no need to proxy assets
if (
/\.(png|jpe?g|gif|svg|webp|ico|bmp|tiff|avif)$/i.test(requestUrl.pathname)
) {
return fetch(request)
}

const config = getConfig(env)
const clientIp = request.headers.get("CF-Connecting-IP") || ""
const cookies = parseCookies(request.headers.get("Cookie"))
const hasDebugCookie = cookies[config.debugCookie] === "1"
const enableRequested =
requestUrl.searchParams.get("cf_local_debug") === "1"
const disableRequested =
requestUrl.searchParams.get("cf_local_debug") === "0"
const ipMatches = clientIp === config.debugIp

if (enableRequested && ipMatches) {
const cleanUrl = new URL(requestUrl)
cleanUrl.searchParams.delete("cf_local_debug")
return buildRedirectResponse(
cleanUrl,
config.debugCookie,
"1",
config.debugMaxAge
)
}

if (disableRequested) {
const cleanUrl = new URL(requestUrl)
cleanUrl.searchParams.delete("cf_local_debug")
return buildRedirectResponse(cleanUrl, config.debugCookie, "0", 0)
}

// Not in debug mode — pass through to the real origin
if (!(hasDebugCookie && ipMatches)) return fetch(request)

// Proxy to the local tunnel
const debugOrigin = new URL(config.debugOrigin)
const targetUrl = new URL(requestUrl)
targetUrl.protocol = debugOrigin.protocol
targetUrl.hostname = debugOrigin.hostname
targetUrl.port = debugOrigin.port
targetUrl.searchParams.delete("cf_local_debug")

const headers = new Headers(request.headers)
headers.set("Host", debugOrigin.hostname)
headers.set("x-debug-via", "cloudflare-worker")
headers.set("x-original-host", requestUrl.hostname)
headers.set("x-forwarded-proto", "https")
headers.set("x-forwarded-for", clientIp)
// x-forwarded-host is intentionally omitted — some tunnel providers (ngrok, Cloudflare Tunnel)
// drop connections if it doesn't match the SNI hostname

const proxiedRequest = new Request(targetUrl.toString(), {
method: request.method,
headers,
body: request.body,
redirect: "manual",
})

const upstream = await fetch(proxiedRequest)
const response = new Response(upstream.body, upstream)
response.headers.set("Cache-Control", "private, no-store")
return response
},
}
```

A few design decisions worth noting:

- **Image passthrough** — image requests skip the worker entirely to avoid unnecessary tunnel traffic for static assets.
- **IP lock** — the debug cookie can only be set from `DEBUG_IP`, so no other visitor can accidentally activate it.
- **`x-forwarded-host` is omitted** — Cloudflare Tunnel (and ngrok) may drop connections when this header doesn't match the tunnel's SNI hostname.
- **`redirect: "manual"`** — redirects from the tunnel are forwarded as-is rather than followed internally, which matters for things like SAML assertion callbacks.

**Deploy**

```bash
npm install
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be done inside the container, in the folder chosen?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should wrangler have been installed inside the container?

npm run deploy
Comment on lines +198 to +199
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we work with DDEV, does it make sense to use ddev npm here instead?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the worker code is not inside ddev, at least not on the perpective of the blog post.

```

**Set secrets**

Secrets are set via the CLI and stored encrypted by Cloudflare — nothing sensitive ever lives in source control. The two required ones are the tunnel URL (more on that in the next step) and your public IP:

```bash
# The local tunnel URL from ddev share
npx wrangler secret put DEBUG_ORIGIN
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And the same for ddev npx everywhere?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same response.


# Pipe your current public IP directly — no copy-pasting
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add "You probably want to start with 0.0.0.0, but then after it's working, use your current local IP instead"

curl -s https://api.ipify.org | npx wrangler secret put DEBUG_IP
```

Two optional secrets have sensible defaults:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend pushing this to later and saying specifically "don't do this unless you have a reason to". I did it just because it was in front of me.


```bash
# Cookie name — defaults to cf_local_debug
npx wrangler secret put DEBUG_COOKIE

# Cookie lifetime in seconds — defaults to 3600 (1 hour)
npx wrangler secret put DEBUG_MAX_AGE
```

**Attach the route**

In the Cloudflare dashboard, go to **Workers & Pages → your Worker → Settings → Domains & Routes** and add `myapp.example.com/*`.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a lot more complicated than implied here. Hoping I did it right:

image
Suggested change
In the Cloudflare dashboard, go to **Workers & Pages → your Worker → Settings → Domains & Routes** and add `myapp.example.com/*`.
In the Cloudflare dashboard for your site, go to **Workers & Pages → your Worker → Settings → Domains & Routes** and add `myapp.example.com/*`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made the mistake of using the default *.randyfay.com/* instead of the needed randyfay.com/*


> **Important:** This only works if your DNS record is set to **Proxied** (orange cloud) in Cloudflare. If it's DNS-only, the Worker never sees the traffic.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
> **Important:** This only works if your DNS record is set to **Proxied** (orange cloud) in Cloudflare. If it's DNS-only, the Worker never sees the traffic.
:::warning[Important]
This only works if your DNS record is set to **Proxied** (orange cloud) in Cloudflare. If it's DNS-only, the Worker never sees the traffic.
:::


## Starting the Tunnel

Each time you want to debug, start your DDEV project and expose it:

```bash
ddev start
ddev share --provider=cloudflared
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we need the --host-header and the additional_fqdns

```

`ddev share` will print a public URL like `https://care-assessment-divine-forestry.trycloudflare.com`. Copy it and update the Worker secret:

```bash
npx wrangler secret put DEBUG_ORIGIN
# paste the tunnel URL when prompted
```

> **Note:** The tunnel URL is ephemeral — it changes every time you run `ddev share`. Remember to update `DEBUG_ORIGIN` each session.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
> **Note:** The tunnel URL is ephemeral — it changes every time you run `ddev share`. Remember to update `DEBUG_ORIGIN` each session.
:::note
The tunnel URL is ephemeral — it changes every time you run `ddev share`. Remember to update `DEBUG_ORIGIN` each session.
:::


## Activating Debug Mode

With the Worker deployed, the route configured, and the tunnel running, visit your production URL with the toggle parameter:

```
https://myapp.example.com/?cf_local_debug=1
```

The Worker checks your IP against `DEBUG_IP`, sets the cookie, and redirects you to the clean URL. Your browser is now transparently receiving responses from your local DDEV environment.

## Debugging with Xdebug

Since traffic is now being served from your local DDEV, Xdebug works exactly as it normally would:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not quite. If using PhpStorm, you have to add "Servers" that match the names of the sites. With VS Code, not needed.


```bash
ddev xdebug on
```

Set a breakpoint in your IDE, trigger the request (or let the IdP do it for you), and the debugger will pause execution right where you need it — even though the URL in the browser says `myapp.example.com`.

## Wrapping Up

This setup scratches a very specific itch. No staging deploys, no IdP reconfiguration, no "works on my machine" guesswork. Just your real production URL, your local DDEV environment, and Xdebug ready to catch whatever comes in.

The code is intentionally simple and easy to adapt to different workflows, cookie strategies, or tunnel providers.

The full worker source is at [hanoii/cloudflare-worker-ddev](https://github.com/hanoii/cloudflare-worker-ddev).
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is AI assisted

Please add something similar to this at the end of the article:

Suggested change
The full worker source is at [hanoii/cloudflare-worker-ddev](https://github.com/hanoii/cloudflare-worker-ddev).
The full worker source is at [hanoii/cloudflare-worker-ddev](https://github.com/hanoii/cloudflare-worker-ddev).
---
_This article was edited and refined with assistance from Claude Code._

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should go way up higher, before any code, with caveat that the repo will likely be better maintained than the pasted code.

Loading