CI/CD für Cloudflare Pages & Workers

Saubere Deployments über GitHub Actions – und welche Variante Best Practice ist

12 Minuten
CI/CD für Cloudflare Pages & Workers
#Cloudflare #CI/CD #GitHub Actions #Wrangler

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.toml hat

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:

  1. Cloudflare Dashboard → Workers & Pages → Dein Projekt
  2. Settings → Builds & deployments
  3. „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 Permissions
  • CLOUDFLARE_ACCOUNT_ID – Deine Account ID (findest du im Dashboard)

API Token erstellen:

  1. Cloudflare Dashboard → My Profile → API Tokens
  2. „Create Token” → „Edit Cloudflare Workers” Template
  3. 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:

SchrittOhne CacheMit Cache
npm install45s5s
Build60s20s (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 Wrangler
  • CLOUDFLARE_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-TypWo speichernBeispiele
Build-ZeitGitHub SecretsCLOUDFLARE_API_TOKEN, NPM-Tokens
LaufzeitWrangler SecretsDB-Credentials, API-Keys, Auth-Secrets
Nicht-sensitivwrangler.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.

Weiterführende Ressourcen