← Back to TIL
DE2025-01-29

Cloudflare Pages: Environments mit wrangler.jsonc sauber trennen

#cloudflare #pages #wrangler #d1 #kv #r2 #bindings #deployment #nuxt #nitro

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

  • Productionmy-app-d1, my-app-kv, my-app-r2
  • Previewmy-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=preview wählt nicht automatisch env.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_dir aus der Config entfernen → Dashboard-Bindings bleiben editierbar
  • Problem: Bindings nicht im Git, postbuild-Hack nötig um pages_build_output_dir aus dem Nitro-Output zu entfernen
  • Fragil und intransparent

env-Sektion direkt nutzen (naiver Ansatz):

  • wrangler.jsonc mit env.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): env wird entfernt, Root-Bindings bleiben → Prod-Deploy
  • Mit preview: Preview-Bindings überschreiben die Root-Bindings, env wird 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

AspektAlter Ansatz (Dashboard)Neuer Ansatz (wrangler.jsonc)
Source of TruthDashboardGit-Repo
Versioniert
Environment-TrennungManuell im DashboardAutomatisch via Postbuild
Reproduzierbar
Workaround nötigpages_build_output_dir entfernenenv 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.

intelliBrain

AI-augmented software development. Based in Zürich, working globally.

© 2026 intelliBrain GmbH. All rights reserved.Imprint
BUILT WITH 🧠 + AI