Cloudflare Pages: Environments mit wrangler.jsonc sauber trennen
Wenn du ein Cloudflare Pages-Projekt mit Nuxt/Nitro betreibst und separate Datenbanken für Production und Preview willst, stehst du vor einem Problem: Cloudflare Pages unterstützt die env-Sektion in der generierten Worker-Config nicht. Hier ist die Lösung, die wir nach einigem Trial-and-Error gefunden haben.
Das Ziel
- Production →
my-app-d1,my-app-kv,my-app-r2 - Preview →
my-app-d1-preview,my-app-kv-preview,my-app-r2-preview - Alles versioniert im Repo, nicht manuell im Dashboard gepflegt
Die Herausforderung
Workers vs. Pages
Bei Cloudflare Workers funktioniert env.preview in der wrangler.toml/jsonc einwandfrei — Wrangler wählt automatisch das richtige Environment.
Bei Cloudflare Pages ist das anders:
wrangler pages deploy --branch=previewwählt nicht automatischenv.preview- Nitro/Nuxt kopiert die gesamte
wrangler.jsonc(inkl.env) in den Build-Output - Cloudflare Pages verweigert den Deploy, wenn die generierte Config eine
env-Sektion enthält
Was NICHT funktioniert
Dashboard als Source of Truth (alter Ansatz):
pages_build_output_diraus der Config entfernen → Dashboard-Bindings bleiben editierbar- Problem: Bindings nicht im Git,
postbuild-Hack nötig umpages_build_output_diraus dem Nitro-Output zu entfernen - Fragil und intransparent
env-Sektion direkt nutzen (naiver Ansatz):
wrangler.jsoncmitenv.preview→ Nitro kopiert alles in den Output → Cloudflare Pages lehnt den Deploy ab mit:Redirected configurations cannot include environments
Die Lösung: wrangler.jsonc + Postbuild-Script
1. wrangler.jsonc als Source of Truth
{
"name": "my-app",
"pages_build_output_dir": "dist",
"compatibility_date": "2025-07-15",
"compatibility_flags": ["nodejs_compat"],
// Production bindings (default)
"d1_databases": [
{ "binding": "DB", "database_name": "my-app-d1", "database_id": "prod-id-hier" }
],
"kv_namespaces": [
{ "binding": "KV", "id": "prod-kv-id-hier" }
],
"r2_buckets": [
{ "binding": "BUCKET", "bucket_name": "my-app-r2" }
],
// Preview overrides — werden vom Postbuild-Script angewendet
"env": {
"preview": {
"d1_databases": [
{ "binding": "DB", "database_name": "my-app-d1-preview", "database_id": "preview-id-hier" }
],
"kv_namespaces": [
{ "binding": "KV", "id": "preview-kv-id-hier" }
],
"r2_buckets": [
{ "binding": "BUCKET", "bucket_name": "my-app-r2-preview" }
]
}
}
}
Die env-Sektion dient als Referenz und wird vom Postbuild-Script ausgewertet — Cloudflare Pages sieht sie nie.
2. Postbuild-Script (scripts/postbuild.mjs)
/**
* Cloudflare Pages unterstützt keine `env`-Sektion in der Worker-Config.
* Dieses Script wendet die richtigen Bindings an und entfernt `env`.
*
* Usage: node scripts/postbuild.mjs [preview]
*/
import { readFileSync, writeFileSync } from 'node:fs'
const env = process.argv[2] // 'preview' oder undefined (= production)
const path = 'dist/_worker.js/wrangler.json'
const config = JSON.parse(readFileSync(path, 'utf8'))
if (env && config.env?.[env]) {
const envConfig = config.env[env]
for (const [key, value] of Object.entries(envConfig)) {
config[key] = value
}
console.log(`✔ Applied "${env}" bindings`)
}
delete config.env
writeFileSync(path, JSON.stringify(config, null, 2))
console.log('✔ Ready for deploy')
Was passiert:
- Ohne Argument (Production):
envwird entfernt, Root-Bindings bleiben → Prod-Deploy - Mit
preview: Preview-Bindings überschreiben die Root-Bindings,envwird entfernt → Preview-Deploy
3. Taskfile.yml für saubere Workflows
version: '3'
tasks:
build:
desc: Build Nuxt app
cmds: [bunx nuxi build]
deploy:
desc: Build & deploy to production
cmds:
- task: build
- node scripts/postbuild.mjs
- bunx wrangler pages deploy dist --project-name=my-app --branch=main
deploy:preview:
desc: Build & deploy to preview
cmds:
- task: build
- node scripts/postbuild.mjs preview
- bunx wrangler pages deploy dist --project-name=my-app --branch=preview
deploy:all:
desc: Deploy to both environments
cmds:
- task: build
- node scripts/postbuild.mjs
- bunx wrangler pages deploy dist --project-name=my-app --branch=main
- node scripts/postbuild.mjs preview
- bunx wrangler pages deploy dist --project-name=my-app --branch=preview
Wichtig bei deploy:all: Es wird nur einmal gebaut. Das Postbuild-Script modifiziert nur die generierte wrangler.json im dist/-Ordner — das geht schnell und erfordert keinen Rebuild.
Deploy-Workflow
task deploy # Production
task deploy:preview # Preview
task deploy:all # Beide
Secrets
Secrets (API-Keys, Auth-Secrets) gehören nicht in die wrangler.jsonc. Sie werden pro Environment im Cloudflare Dashboard gesetzt:
# Oder via CLI:
echo "dein-secret" | wrangler pages secret put SECRET_NAME --project-name=my-app
Im Dashboard: Workers & Pages → Projekt → Settings → Environment Variables → Production/Preview wählen
Fazit
| Aspekt | Alter Ansatz (Dashboard) | Neuer Ansatz (wrangler.jsonc) |
|---|---|---|
| Source of Truth | Dashboard | Git-Repo |
| Versioniert | ❌ | ✅ |
| Environment-Trennung | Manuell im Dashboard | Automatisch via Postbuild |
| Reproduzierbar | ❌ | ✅ |
| Workaround nötig | pages_build_output_dir entfernen | env strippen |
Der Postbuild-Schritt ist ein Workaround für eine Cloudflare Pages Limitation — kein Hack. Sobald Cloudflare Pages env-Sektionen nativ unterstützt, kann das Script einfach gelöscht werden.