Cloudflare macht es verlockend einfach: Repository verbinden, pushen, fertig. Aber spätestens wenn der Build komplexer wird, SSR im Spiel ist oder du Linting-Gates brauchst, reicht dieses Standard-Setup nicht mehr aus.
Hier schauen wir uns die drei gängigen Deploy-Varianten an – und welche davon für ein professionelles Setup wirklich sinnvoll ist.
Das Szenario
Du hast ein Projekt, das:
- als Cloudflare Pages oder Workers läuft (mit oder ohne SSR)
- im Hintergrund Workers nutzt (Functions-Directory oder klassischer Worker)
- über GitHub gepflegt wird
- bereits eine
wrangler.tomlhat
Das heißt: Ein Hybrid-Projekt mit statischen Assets und Worker/SSR-Code. Genau dort merkt man schnell, dass man den Build-Prozess lieber selbst kontrollieren will.
Die 3 Deployment-Varianten
Option A: Cloudflare baut & deployt (Standard)
So läuft es direkt nach dem Verbinden des Repos:
Push → Cloudflare zieht Code → Cloudflare baut → Deploy
Vorteile:
- Sehr simpel
- Build läuft in Cloudflare ohne eigenen CI
- Keine Pflege von GitHub Actions nötig
Nachteile:
- Linting und statische Checks laufen erst während des Deploys – nicht vorher als Gate
- Build-Fehler werden erst beim Deployment sichtbar, nicht in der PR
- Kein reproduzierbares Build-Artefakt (Cloudflares Umgebung kann sich ändern)
Für schnelle Prototypen okay – für ernsthafte Projekte eher nicht.
Cloudflare bietet mittlerweile gute Kontrolle über die Build-Umgebung: Node.js-Versionen lassen sich direkt unter Settings > Build & deployments > Build system version auswählen (aktuell V3 mit Node.js 22 als Default). Alternativ funktionieren .nvmrc-Dateien oder die NODE_VERSION Environment Variable. npm, pnpm und yarn werden alle unterstützt. Der eigentliche Nachteil von Option A ist der Workflow: Fehler siehst du erst nach dem Push, nicht schon in der PR.
Option B: GitHub baut, Cloudflare deployed (kein Cloudflare-Build)
Hier übernimmst du den kompletten Build selbst und gibst Cloudflare nur das fertige Artefakt. Cloudflare baut nichts mehr – es fungiert nur noch als Hosting und CDN. Das ist die Best Practice für Pages-Projekte.
Push → GitHub Actions (Lint + Tests + Build) → Upload fertiger dist-Ordner → Cloudflare verteilt
Die cloudflare/pages-action lädt den Build-Output (dist/) direkt hoch. Die Git-Integration muss dafür deaktiviert sein, sonst würde Cloudflare trotzdem versuchen zu bauen.
Workflow-Beispiel:
name: Deploy to Cloudflare Pages
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Build
run: npm run build
- name: Deploy to Cloudflare Pages
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: meine-app
directory: dist
Vorteile:
- Deploy nur bei erfolgreichem Lint/Build – fehlerhafte Commits erreichen nie die Produktion
- Schnelleres Feedback: Fehler erscheinen direkt in der PR, nicht erst nach dem Merge
- Build-Artefakte und Dependencies können gecacht werden (siehe Abschnitt Caching)
- Reproduzierbare Builds: Gleicher Code → gleiches Ergebnis
- Ideal für Astro, SvelteKit, Next.js auf Pages
Nachteile:
- Initiale CI-Konfiguration nötig (einmaliger Aufwand)
- Git-Integration in Cloudflare muss deaktiviert werden
Option C: wrangler deploy (für Workers & SSR, kein Cloudflare-Build)
Wenn dein Projekt SSR, persistente Worker-Skripte oder Durable Objects nutzt, kommt Wrangler ins Spiel. Auch hier gilt: Der Build läuft komplett in GitHub, Wrangler deployt nur das Ergebnis.
Push → GitHub Actions (Lint + Tests + Build) → wrangler deploy → Cloudflare verteilt
Workflow-Beispiel:
name: Deploy Worker
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint & Test
run: |
npm run lint
npm run test
- name: Build
run: npm run build
- name: Deploy Worker
run: npx wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
Vorteile:
- Volle Kontrolle über Worker-Deployment
- Assets und Worker können gemeinsam oder getrennt deployed werden
- Funktioniert perfekt mit
wrangler.toml - Unterstützt alle Worker-Features (Durable Objects, Cron, Queues)
Typische Einsatzfälle:
- SSR-Frameworks (Astro SSR, SvelteKit, Next.js)
- API-Endpoints über Workers
- Edge-Funktionen, Cron-Triggers, KV/DO-Anbindung
Best Practice: Option B + C kombiniert
Für die meisten ernsthaften Projekte gilt:
GitHub baut → GitHub validiert → GitHub deployed
Cloudflare kümmert sich nur ums Hosting.
Warum?
- Volle Kontrolle über den gesamten Prozess
- Reproduzierbarer Build
- Linting und Testing als Gate
- Schnelle Builds durch Caching
- Keine versteckten Fehler in Cloudflares Build-Umgebung
Wenn du Worker/SSR nutzt → wrangler deploy verwenden.
Wenn du rein statisch bleibst → die Pages Action reicht.
Setup für ein Pages+Workers Projekt
1. Cloudflare Git-Integration deaktivieren
Damit nur GitHub Actions deployen, musst du die automatische Git-Integration in Cloudflare deaktivieren:
- Cloudflare Dashboard → Workers & Pages → Dein Projekt
- Settings → Builds & deployments
- „Pause deployments” oder Git-Verbindung trennen
2. GitHub Secrets einrichten
Unter Repository → Settings → Secrets and variables → Actions:
CLOUDFLARE_API_TOKEN– API Token mit Workers/Pages PermissionsCLOUDFLARE_ACCOUNT_ID– Deine Account ID (findest du im Dashboard)
API Token erstellen:
- Cloudflare Dashboard → My Profile → API Tokens
- „Create Token” → „Edit Cloudflare Workers” Template
- Permissions anpassen: Account + Zone Permissions für dein Projekt
3. Workflow definieren
Vollständiges Beispiel für ein Astro-Projekt mit SSR:
name: Build and Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
NODE_VERSION: '20'
jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Type Check
run: npm run type-check
- name: Test
run: npm run test
deploy:
needs: lint-and-test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Deploy
run: npx wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
4. Preview Deployments
Für Pull Requests kannst du Preview-Deployments einrichten:
deploy-preview:
needs: lint-and-test
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Deploy Preview
id: deploy
run: |
OUTPUT=$(npx wrangler deploy --env preview 2>&1)
echo "$OUTPUT"
URL=$(echo "$OUTPUT" | grep -oP 'https://[^\s]+\.workers\.dev')
echo "url=$URL" >> $GITHUB_OUTPUT
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- name: Comment PR
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '🚀 Preview deployed: ${{ steps.deploy.outputs.url }}'
})
Caching für schnellere Builds
Caching ist einer der größten Vorteile von GitHub Actions gegenüber Cloudflares eingebautem Build. Richtig konfiguriert spart es bei jedem Build mehrere Minuten.
Dependencies cachen
Der häufigste Fall: node_modules muss nicht jedes Mal neu heruntergeladen werden. Die setup-node Action macht das automatisch:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # Cached node_modules basierend auf package-lock.json
Was passiert hier? GitHub speichert den node_modules-Ordner und stellt ihn wieder her, wenn sich package-lock.json nicht geändert hat. Bei einem typischen Projekt spart das 30-60 Sekunden pro Build.
Für pnpm oder yarn:
# pnpm
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
# yarn
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'
Build-Artefakte cachen
Manche Frameworks haben eigene Caches, die den Build beschleunigen. Astro zum Beispiel speichert verarbeitete Assets in .astro/:
- name: Cache Build
uses: actions/cache@v4
with:
path: |
dist
.astro
key: build-${{ hashFiles('src/**', 'astro.config.mjs') }}
restore-keys: build-
Der key ist entscheidend: Er basiert auf einem Hash aller Quelldateien. Ändert sich eine Datei in src/, wird der Cache invalidiert. Die restore-keys erlauben einen Fallback auf ältere Caches, falls kein exakter Match existiert.
Wie viel bringt das?
Ein Vergleich für ein typisches Astro-Projekt:
| Schritt | Ohne Cache | Mit Cache |
|---|---|---|
| npm install | 45s | 5s |
| Build | 60s | 20s (bei teilweisem Cache) |
| Gesamt | ~2 min | ~30s |
Bei 20 Deployments pro Tag sind das 30 Minuten gesparte Build-Zeit – und weniger Warten auf Feedback.
Monorepo-Setup
Bei Monorepos mit mehreren Packages:
- name: Install dependencies
run: npm ci
working-directory: packages/web
- name: Build
run: npm run build
working-directory: packages/web
- name: Deploy
run: npx wrangler deploy
working-directory: packages/web
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
Oder mit Path-Filtern:
on:
push:
branches: [main]
paths:
- 'packages/web/**'
- '.github/workflows/deploy-web.yml'
Rollback-Strategie
Deployments können schiefgehen. Mit GitHub Actions hast du mehrere Möglichkeiten, schnell zurückzurollen.
Über das Cloudflare Dashboard
Der einfachste Weg: Im Cloudflare Dashboard unter Workers & Pages → Dein Projekt → Deployments siehst du alle bisherigen Deployments. Jedes hat einen “Rollback”-Button, der dieses Deployment sofort wieder live schaltet.
Über Git
Da der Build reproduzierbar ist, kannst du auch einfach den letzten funktionierenden Commit neu deployen:
# Letzten Commit reverten
git revert HEAD
git push
# Oder: Auf bestimmten Commit zurücksetzen
git reset --hard <commit-hash>
git push --force # Vorsicht: überschreibt History
Der GitHub Actions Workflow läuft automatisch und deployt den alten Stand.
Automatisches Rollback bei Fehlern
Für kritische Projekte kannst du einen Health-Check einbauen:
- name: Deploy
run: npx wrangler deploy
- name: Health Check
run: |
sleep 10 # Warten bis Deployment propagiert
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://meine-app.workers.dev/health)
if [ "$STATUS" != "200" ]; then
echo "Health check failed, rolling back..."
# Hier könnte ein Rollback-Script stehen
exit 1
fi
Branch-Deployments (Environments)
Für größere Projekte willst du verschiedene Umgebungen: Development, Staging, Production. Cloudflare und GitHub Actions machen das einfach.
Wrangler Environments
In der wrangler.toml definierst du Environments:
name = "meine-app"
compatibility_date = "2025-01-01"
# Production (Default)
[vars]
ENVIRONMENT = "production"
API_URL = "https://api.example.com"
# Staging Environment
[env.staging]
name = "meine-app-staging"
[env.staging.vars]
ENVIRONMENT = "staging"
API_URL = "https://staging-api.example.com"
# Preview Environment
[env.preview]
name = "meine-app-preview"
[env.preview.vars]
ENVIRONMENT = "preview"
API_URL = "https://staging-api.example.com"
Branch-basiertes Deployment
name: Deploy
on:
push:
branches: [main, staging, develop]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup & Build
run: |
npm ci
npm run build
- name: Deploy Production
if: github.ref == 'refs/heads/main'
run: npx wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- name: Deploy Staging
if: github.ref == 'refs/heads/staging'
run: npx wrangler deploy --env staging
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
- name: Deploy Preview
if: github.ref == 'refs/heads/develop'
run: npx wrangler deploy --env preview
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
GitHub Environments
Für zusätzliche Kontrolle kannst du GitHub Environments nutzen. Diese erlauben:
- Approval-Gates: Jemand muss das Deployment bestätigen
- Environment-spezifische Secrets: Unterschiedliche API-Keys pro Environment
- Deployment-History: Übersicht, was wann wohin deployed wurde
deploy-production:
runs-on: ubuntu-latest
environment:
name: production
url: https://meine-app.com
steps:
- name: Deploy
run: npx wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
Im Repository unter Settings → Environments kannst du dann Regeln definieren – z.B. dass Production-Deployments eine Genehmigung brauchen.
Secrets-Management
Bei CI/CD gibt es zwei Orte für Secrets: GitHub und Cloudflare. Wann nutzt du was?
GitHub Secrets
Für alles, was der Build-Prozess braucht:
CLOUDFLARE_API_TOKEN– Authentifizierung für WranglerCLOUDFLARE_ACCOUNT_ID– Deine Account-ID- NPM-Tokens für private Packages
- API-Keys für Build-Zeit-Services (z.B. CMS-API für Static Generation)
Einrichten unter Repository → Settings → Secrets and variables → Actions.
Wrangler Secrets
Für alles, was der Worker zur Laufzeit braucht:
# Secret setzen (wird verschlüsselt gespeichert)
npx wrangler secret put DATABASE_URL
npx wrangler secret put AUTH_SECRET
npx wrangler secret put STRIPE_SECRET_KEY
# Für spezifisches Environment
npx wrangler secret put DATABASE_URL --env staging
Diese Secrets sind nur zur Laufzeit im Worker verfügbar (env.DATABASE_URL), nie im Build-Log oder Repository.
Best Practices
| Secret-Typ | Wo speichern | Beispiele |
|---|---|---|
| Build-Zeit | GitHub Secrets | CLOUDFLARE_API_TOKEN, NPM-Tokens |
| Laufzeit | Wrangler Secrets | DB-Credentials, API-Keys, Auth-Secrets |
| Nicht-sensitiv | wrangler.toml [vars] | ENVIRONMENT, API_URL, Feature-Flags |
Wichtig: Secrets aus wrangler secret put werden nicht in der wrangler.toml gespeichert – sie existieren nur verschlüsselt in Cloudflare. Das ist gewollt: Die TOML-Datei kann ins Repository, die echten Secrets nicht.
Secrets in CI/CD setzen
Du kannst Wrangler Secrets auch automatisiert setzen:
- name: Set Runtime Secrets
run: |
echo "${{ secrets.DATABASE_URL }}" | npx wrangler secret put DATABASE_URL
echo "${{ secrets.AUTH_SECRET }}" | npx wrangler secret put AUTH_SECRET
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
Das ist nützlich, wenn sich Secrets ändern und du sie nicht manuell in jedem Environment aktualisieren willst.
Zusammenfassung
Die integrierte Git-Deployment-Funktion ist gut für schnelle Prototypen – aber sobald du echte CI willst, führt kein Weg an GitHub Actions vorbei.
Mit einer Kombination aus:
- GitHub → Build (kontrollierte Umgebung)
- GitHub → Validate (Lint, Tests, Type-Check)
- Wrangler → Deploy (Workers/SSR)
baust du ein CI/CD, das zuverlässig, reproduzierbar und Cloudflare-kompatibel ist.
Der Mehraufwand für die initiale Einrichtung zahlt sich schnell aus: Keine kaputten Deployments mehr, bessere Nachvollziehbarkeit und volle Kontrolle über den gesamten Prozess.