Design wechseln

Next.js CI/CD in der Praxis: Automatisierte Tests und Deployments mit GitHub Actions

Easton editorial illustration: rendering-mode selector

Sie starren auf die scrollenden Logs im Terminal und tippen mechanisch git pull && npm install && npm run build && pm2 restart. Heute schon das dritte Deployment – morgens ein kleiner Bugfix, mittags eine API-Optimierung, jetzt wieder ein paar Style-Anpassungen. Drei Server, auf jedem derselbe Ablauf.

Nach dem letzten Server zeigt die Uhr sieben.

Auf dem Heimweg dachte ich: Es muss einen besseren Weg geben. Nicht nach jedem Code-Change dieselben Schritte auf dem Server wiederholen – und nicht die Sorge, dass ein vergessener Neustart Probleme in Produktion verursacht. Tests? Die wurden oft übersprungen – deployen ging vor, und später kam der Bug, dann alles von vorn.

Mit CI/CD und GitHub Actions hat sich der Workflow grundlegend geändert. Ein Push zu GitHub – Tests, Build und Deployment laufen automatisch. In der Zeit für einen Kaffee ist die neue Version online.

Wenn manuelles Deploy Sie nervt, zeige ich im Folgenden, wie Sie mit GitHub Actions eine automatisierte Next.js-Pipeline aufsetzen – von der Basis-Konfiguration bis zum vollständigen Praxisbeispiel, inklusive Fallstricken und Lösungen.

Warum CI/CD?

Die Schmerzen des manuellen Deployments

Am Anfang meiner Next.js-Projekte sah der Ablauf so aus: lokal entwickeln, kurz testen, per SSH auf den Server, git pull, npm install, npm run build, pm2 restart. Alles glatt – trotzdem gut zehn Minuten.

Oft lief es nicht glatt.

Einmal auf drei Servern deployt: zwei erfolgreich, beim dritten brach die SSH-Verbindung ab – unbemerkt. Am nächsten Tag: die Seite wirkt mal aktuell, mal veraltet – der Load Balancer traf auf den nicht aktualisierten Server. Ein anderes Mal: Code geändert, sofort deployed, Tests vergessen – kritischer Bug in Produktion, eiliges Rollback.

Besonders tückisch bei mehreren Servern: Jeder npm run build erzeugt eine neue Next.js-Build-ID. Drei Server, drei IDs – der Load Balancer verteilt Requests, Next.js erkennt ID-Wechsel und erzwingt einen Hard Refresh. Die UX leidet.

Was CI/CD löst

Kurz gesagt: wiederholbare, fehleranfällige Schritte werden automatisiert.

CI (Continuous Integration) übernimmt Tests. Bei jedem Push laufen Unit-Tests, Typ- und Lint-Checks. Schlägt etwas fehl, gibt es kein Deployment – Sie beheben zuerst den Fehler. So landet fehlerhafter Code seltener in Produktion.

CD (Continuous Deployment) übernimmt Build und Deployment. Tests grün? Build startet automatisch, danach Deployment auf die Server – ohne manuelles Eingreifen.

Mein Workflow heute: lokal coden → Push zu GitHub → Tests → Build → Deploy. Push bis Live: etwa fünf Minuten, ohne am Terminal zu warten.

Zusätzlich: Nachvollziehbarkeit. Jeder Lauf hat Logs – welcher Commit, welche Testergebnisse, welche Server. Bei Problemen finden Sie die Ursache schneller.

GitHub Actions: Basis-Konfiguration

Wie es funktioniert

Workflow, Job, Step – am Anfang wirkt das viel. Die Idee ist einfach:

Workflow = eine komplette Automatisierung (z. B. Test + Build + Deploy), definiert in einer YAML-Datei unter .github/workflows.

Job = eine Aufgabe im Workflow, z. B. „test“ und „deploy“. Jobs können parallel laufen oder per needs nacheinander.

Step = einzelne Aktion im Job: Code auschecken, Dependencies installieren, Tests starten.

Trigger flexibel: bei Push auf main, nur bei Pull Requests, oder per Cron.

Erster Workflow

Ordner .github/workflows anlegen, Datei ci-cd.yml:

name: CI/CD Pipeline

# Trigger: Push auf main
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

Bedeutung: Bei Push auf main läuft auf Ubuntu Checkout, Node 18, npm ci, npm run build.

Nach dem Commit unter Actions im Repository den Lauf sehen – das erste grüne Häkchen motiviert.

Secrets für sensible Daten

Für Server-Deploys brauchen Sie IP, SSH-Keys, API-Tokens. Nicht direkt in die YAML schreiben – das Repository wäre exponiert.

Stattdessen Secrets: Repository → Settings → Secrets and variables → Actions → New repository secret, z. B. SERVER_HOST mit der Server-IP.

Im Workflow: ${{ secrets.SERVER_HOST }} – GitHub ersetzt den Wert; in Logs wird maskiert.

Automatisierte Tests

Test-Setup

Alles, was automatisierbar ist, automatisieren: ESLint, TypeScript, Jest – damit fangen die meisten Probleme früh ab.

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'npm'  # npm-Cache beschleunigt Folgeläufe

      - name: Install dependencies
        run: npm ci

      - name: Lint check
        run: npm run lint

      - name: Type check
        run: npm run type-check

      - name: Run tests
        run: npm run test -- --coverage

cache: 'npm' cached node_modules – ab dem zweiten Lauf deutlich schneller.

Tests beschleunigen

Anfangs dauerte ein Lauf über zehn Minuten – nach Optimierung etwa drei.

Caching – npm und Next.js-Build-Cache:

- name: Cache Next.js build
  uses: actions/cache@v3
  with:
    path: |
      ~/.npm
      .next/cache
    key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}

Next.js muss nicht jede Seite jedes Mal neu bauen.

Parallelisierung – unabhängige Checks in separate Jobs:

jobs:
  lint:
    runs-on: ubuntu-latest
    steps: [...]  # nur Lint

  test:
    runs-on: ubuntu-latest
    steps: [...]  # nur Unit-Tests

  type-check:
    runs-on: ubuntu-latest
    steps: [...]  # nur Typen

Die drei Jobs starten parallel; die Gesamtzeit entspricht dem langsamsten Job.

Typische Fallen

Tests lokal grün, in Actions rot? Bei PRs merged GitHub Actions oft PR-Branch und Ziel-Branch zu einem temporären Commit – den gibt es lokal nicht.

Lösung beim Checkout:

- uses: actions/checkout@v4
  with:
    ref: ${{ github.head_ref }}  # Original-PR-Branch

Timeout: Langsame E2E-Tests – timeout-minutes erhöhen:

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 15  # Standard: 6

Automatisches Deployment

Vercel: der einfachste Weg

Projekt auf Vercel? Nach Verknüpfung mit GitHub deployt jeder Push oft schon automatisch.

Feinere Kontrolle (z. B. Deploy nur nach grünen Tests) – offizielle Vercel Action:

jobs:
  deploy:
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main'

    steps:
      - uses: actions/checkout@v4

      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          vercel-args: '--prod'

Token, Org-ID und Project-ID aus dem Vercel-Dashboard in GitHub Secrets legen.

Vercel erstellt pro PR oft Preview-URLs – Änderungen vor dem Merge prüfen.

Eigenes Hosting: flexibler, etwas aufwendiger

Eigene Server geben mehr Kontrolle. SSH einrichten, GitHub Actions führt Deploy-Befehle remote aus.

SSH-Key lokal erzeugen:

ssh-keygen -t ed25519 -C "github-actions"

Public Key in ~/.ssh/authorized_keys auf dem Server, Private Key als GitHub Secret (z. B. SSH_PRIVATE_KEY).

jobs:
  deploy:
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main'

    steps:
      - name: Deploy to server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /var/www/my-nextjs-app
            git pull origin main
            npm install
            npm run build
            pm2 restart nextjs-app

Pull, Install, Build, Restart – ohne manuelles SSH.

Einheitliche Build-ID bei mehreren Servern

Bei Load Balancing müssen alle Instanzen dieselbe Build-ID haben – sonst Hard Refreshes.

Einmal bauen, Artefakte verteilen:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
      - run: npm ci
      - run: npm run build

      - name: Upload build artifacts
        uses: actions/upload-artifact@v3
        with:
          name: next-build
          path: |
            .next
            public

  deploy:
    runs-on: ubuntu-latest
    needs: build
    strategy:
      matrix:
        server: [server1, server2, server3]

    steps:
      - name: Download build artifacts
        uses: actions/download-artifact@v3
        with:
          name: next-build

      - name: Deploy to ${{ matrix.server }}
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets[format('{0}_HOST', matrix.server)] }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          source: ".next,public"
          target: "/var/www/my-nextjs-app"

Ein Build, identische Artefakte auf allen Servern – konsistente Build-ID.

Optimierung und Best Practices

Wenn der Build scheitert

Automatisierung hilft nur, wenn Sie Fehler sehen. Push erfolgreich, Deploy fehlgeschlagen – ohne Benachrichtigung merken Sie es zu spät.

Slack-Benachrichtigung bei Fehler:

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      [...Deploy-Schritte...]

      - name: Notify on failure
        if: failure()
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          text: 'Deployment fehlgeschlagen – bitte prüfen'
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}
          channel: '#deploy-notifications'

Branch-Strategie: Staging und Produktion

develop → Testumgebung, main → Produktion:

on:
  push:
    branches:
      - main
      - develop

jobs:
  deploy-staging:
    if: github.ref == 'refs/heads/develop'
    runs-on: ubuntu-latest
    steps:
      - [...Staging-Deploy...]

  deploy-production:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - [...Produktions-Deploy...]

Entwicklung auf develop, nach Prüfung Merge nach main.

Manuelle Freigabe für Produktion – environment im Job, Approver im Repository:

jobs:
  deploy-production:
    runs-on: ubuntu-latest
    environment:
      name: production
    steps: [...]

Deploy pausiert bis zur Freigabe – sinnvoll für kritische Projekte.

Rollback

Trotz Tests kann Produktion Probleme haben – schnelles Rollback zählt.

Einfaches Muster: Tags pro Release, Artefakte versionieren, workflow_dispatch mit Tag-Eingabe:

on:
  workflow_dispatch:
    inputs:
      tag:
        description: 'Tag für Rollback'
        required: true

jobs:
  rollback:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.inputs.tag }}

      - name: Deploy
        [...normaler Deploy-Ablauf...]

In Actions „Run workflow“, Tag eingeben – Rollback startet.

Umgebungsvariablen

API-URLs, DB-Strings – nicht hardcoden, nicht ins Git.

Empfehlung:

  1. Sensible Werte in GitHub Secrets
  2. Im Workflow als env injizieren
  3. Next.js liest sie beim Build
- name: Build
  run: npm run build
  env:
    NEXT_PUBLIC_API_URL: ${{ secrets.API_URL }}
    DATABASE_URL: ${{ secrets.DATABASE_URL }}

Pro Umgebung andere Secrets – Code bleibt gleich.

Praxisbeispiel: vollständige Konfiguration

Diese Pipeline kombiniert Test, Build und Deploy – als Vorlage anpassbar:

name: Next.js CI/CD

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Lint check
        run: npm run lint

      - name: Type check
        run: npm run type-check

      - name: Run tests
        run: npm run test -- --coverage

  build:
    runs-on: ubuntu-latest
    needs: test
    if: github.event_name == 'push'

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'

      - name: Cache dependencies and build
        uses: actions/cache@v3
        with:
          path: |
            ~/.npm
            .next/cache
          key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build
        env:
          NEXT_PUBLIC_API_URL: ${{ secrets.API_URL }}

      - name: Upload build artifacts
        uses: actions/upload-artifact@v3
        with:
          name: next-build
          path: |
            .next
            public
            package.json

  deploy-staging:
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/develop'

    steps:
      - name: Download build artifacts
        uses: actions/download-artifact@v3
        with:
          name: next-build

      - name: Deploy to staging server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.STAGING_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /var/www/staging
            pm2 stop nextjs-app || true
            rm -rf .next public
            pm2 start npm --name "nextjs-app" -- start
            pm2 save

      - name: Notify Slack
        if: always()
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          text: 'Staging-Deployment ${{ job.status == "success" && "erfolgreich" || "fehlgeschlagen" }}'
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}

  deploy-production:
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/main'
    environment:
      name: production

    steps:
      - name: Download build artifacts
        uses: actions/download-artifact@v3
        with:
          name: next-build

      - name: Deploy to production server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.PROD_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /var/www/production
            pm2 stop nextjs-app || true
            rm -rf .next public
            pm2 start npm --name "nextjs-app" -- start
            pm2 save

      - name: Notify Slack
        if: always()
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          text: 'Produktions-Deployment ${{ job.status == "success" && "erfolgreich" || "fehlgeschlagen" }}'
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}

PRs: nur Tests. develop → Staging, main → Produktion (mit Approval). Slack bei jedem Deploy.

Secrets im Repository:

  • API_URL
  • STAGING_HOST / PROD_HOST
  • SERVER_USER
  • SSH_PRIVATE_KEY
  • SLACK_WEBHOOK (optional)

Danach: coden, committen, pushen – der Rest läuft automatisch.

Fazit

Vom manuellen Deploy zur Automatisierung hat meinen Alltag verändert: keine wiederholten Server-Befehle, keine vergessenen Tests, kein Warten am Terminal.

GitHub Actions CI/CD braucht anfangs Setup-Zeit – lohnt sich dauerhaft. Mit minimaler Konfiguration starten, Tests und Build stabilisieren, dann Deploy, Benachrichtigungen und Rollback ergänzen.

Push, Kaffee, neue Version online – wer es einmal erlebt hat, will selten zurück zum manuellen Weg.

Legen Sie los: erste Workflow-Datei im Next.js-Projekt, unter Actions den ersten grünen Lauf ansehen – dann wird der Unterschied klar.

Next.js CI/CD: Vollständiger Konfigurationsablauf

Von der GitHub-Actions-Workflow-Datei bis zu Test-, Build- und Deploy-Schritten

⏱️ Estimated time: 2 hr

  1. 1

    Step 1: GitHub-Actions-Workflow anlegen

    Datei .github/workflows/deploy.yml erstellen:
    ```yaml
    name: Deploy

    on:
    push:
    branches: [main]

    jobs:
    build-and-deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - uses: actions/setup-node@v3
    with:
    node-version: '18'
    - run: npm install
    - run: npm run build
    - run: npm test
    ```

    Wichtig:
    • Trigger: Push auf main
    • Laufzeit: ubuntu-latest
    • Schritte: checkout → setup-node → install → build → test
  2. 2

    Step 2: Test-Schritte konfigurieren

    Tests hinzufügen:
    ```yaml
    - name: Run tests
    run: npm test

    - name: Type check
    run: npm run type-check

    - name: Lint
    run: npm run lint
    ```

    Wichtig:
    • Fehlgeschlagene Tests blockieren Deploy
    • Typen- und Lint-Checks sichern Qualität

    Vorteile:
    • Fehlerhafter Code erreicht Produktion seltener
    • Checks laufen automatisch – nichts vergessen
  3. 3

    Step 3: Build-Schritt konfigurieren

    Build:
    ```yaml
    - name: Build
    run: npm run build
    env:
    NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}
    ```

    Cache:
    ```yaml
    - name: Cache dependencies
    uses: actions/cache@v3
    with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    ```

    Wichtig:
    • Umgebungsvariablen setzen
    • Cache für schnellere Builds
    • Build-Ausgabe prüfen
  4. 4

    Step 4: Deploy-Schritt konfigurieren

    SSH-Deploy:
    ```yaml
    - name: Deploy to server
    uses: appleboy/ssh-action@master
    with:
    host: ${{ secrets.HOST }}
    username: ${{ secrets.USERNAME }}
    key: ${{ secrets.SSH_KEY }}
    script: |
    cd /path/to/app
    git pull
    npm install
    npm run build
    pm2 restart app
    ```

    Multi-Server:
    ```yaml
    - name: Deploy to servers
    uses: appleboy/ssh-action@master
    with:
    host: ${{ secrets.HOSTS }}
    username: ${{ secrets.USERNAME }}
    key: ${{ secrets.SSH_KEY }}
    script: |
    cd /path/to/app
    git pull
    npm install
    # Artefakte aus GitHub Actions verwenden
    pm2 restart app
    ```

    Wichtig:
    • SSH-Key-Authentifizierung
    • Einheitliche Build-ID (zentral in Actions bauen)
    • Kein paralleles Build auf jedem Server

FAQ

Warum brauche ich CI/CD?
Schmerzen beim manuellen Deploy:
• Nach jedem Change dieselben Server-Schritte
• Vergessene Neustarts oder Tests
• Fehler bei Multi-Server-Deployments
• Unterschiedliche Build-IDs → Hard Refresh

Vorteile von CI/CD:
• Automatisierte Tests, Builds, Deployments
• Push → Live
• Weniger manuelle Fehler
• Einheitliche Build-ID
• Höhere Entwicklerproduktivität

Praxis:
• Drei Server – einer nicht aktualisiert, Load Balancer zeigt alte Version
• Deploy ohne Tests – kritischer Bug in Produktion

Lösung: gesamten Ablauf mit GitHub Actions automatisieren.
Wie konfiguriere ich GitHub Actions?
Datei .github/workflows/deploy.yml:
```yaml
name: Deploy

on:
push:
branches: [main]

jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm install
- run: npm run build
- run: npm test
```

Wichtig:
• Trigger: Push auf main
• ubuntu-latest
• checkout → setup-node → install → build → test

Secrets:
• Settings → Secrets: HOST, USERNAME, SSH_KEY usw.
• Für SSH-Deploy

Tipp: minimal starten, Tests und Build zum Laufen bringen, dann Deploy ergänzen.
Wie vermeide ich unterschiedliche Build-IDs bei mehreren Servern?
Problem: Jeder Server baut selbst → verschiedene IDs → Hard Refresh hinter dem Load Balancer.

Lösung: zentral bauen

In GitHub Actions:
```yaml
- name: Build
run: npm run build

- name: Deploy to servers
uses: appleboy/ssh-action@master
with:
script: |
cd /path/to/app
git pull
# Artefakte aus GitHub Actions
pm2 restart app
```

Oder mit Artefakten:
```yaml
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: build
path: .next

- name: Deploy to servers
uses: appleboy/ssh-action@master
with:
script: |
# Artefakte herunterladen und deployen
```

Wichtig:
• Ein Build in Actions
• Gleiche Artefakte auf allen Servern
• Kein Build pro Server

Vorteil: konsistente ID, bessere UX.
Wie richte ich Test-Schritte ein?
```yaml
- name: Run tests
run: npm test

- name: Type check
run: npm run type-check

- name: Lint
run: npm run lint
```

Wichtig:
• Fehlgeschlagene Tests stoppen Deploy
• Typen und Lint für Qualität

Vorteile:
• Weniger fehlerhafte Releases
• Automatische Checks
• Bessere Codequalität

Tipp: einfach starten, Coverage schrittweise erhöhen.
Wie konfiguriere ich Deploy-Benachrichtigungen?
Slack:
```yaml
- name: Notify Slack
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: 'Deployment abgeschlossen'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
```

E-Mail:
```yaml
- name: Send email
uses: dawidd6/action-send-mail@v3
with:
to: [email protected]
subject: 'Deployment abgeschlossen'
body: 'Produktions-Deployment erfolgreich'
```

Wichtig:
• Erfolg und Fehler melden
• Version, Zeit, Umgebung angeben
• Team schnell informieren

Tipp: Slack für Echtzeit, E-Mail als Backup.
Was sind CI/CD-Best Practices?
Schrittweise:
1. Tests und Build stabilisieren
2. Deploy hinzufügen
3. Benachrichtigungen und Rollback

Nicht alles auf einmal – Basis zuerst, dann verfeinern.

Kontinuierlich verbessern:
• Logs nach jedem Deploy prüfen
• Konfiguration anpassen
• Build-Zeit optimieren
• Test-Coverage erhöhen

Kennzahlen:
• Build-Dauer
• Test-Erfolgsrate
• Deploy-Erfolgsrate
• Rollback-Häufigkeit

CI/CD ist ein Prozess, kein einmaliges Setup.

9 Min. Lesezeit · Veröffentlicht am: 20. Dez. 2025 · Aktualisiert am: 14. Juli 2026

Kommentare

Melde dich mit GitHub an, um einen Kommentar zu hinterlassen

Easton BlogEaston Blog