Design wechseln

GitHub Actions Deployment-Strategien: CD-Pipelines von VPS bis Cloud-Plattform

Einleitung

Um drei Uhr morgens starrte ich auf die GitHub Actions Logs – rote Fehlermeldungen rollten eine nach der anderen hoch. „Host key verification failed“. Wieder ein SSH-Problem.

Das war bereits mein fünfter fehlgeschlagener Deployment-Versuch. Lokal lief alles durch, nach dem Push auf GitHub brach es zusammen. In dem Moment wollte ich am liebsten fluchen – merkte aber auch: Die Wahl der Deployment-Strategie ist deutlich komplexer, als ich dachte.

Ob selbst verwalteter VPS oder Hosting-Plattformen wie Vercel und Cloudflare Pages – jede Option hat ihre Fallstricke. Die falsche Wahl bedeutet mehr nächtliche Debug-Sessions.

Dieser Artikel stellt die gängigen GitHub Actions Deployment-Strategien vor und hilft Ihnen, den passenden Weg für Ihr Projekt zu finden.


VPS-SSH-Deployment: altmodisch, aber zuverlässig

Ehrlich gesagt war ich anfangs skeptisch gegenüber VPS-Deployments. Zu viel Aufwand – SSH-Schlüssel, known_hosts, rsync-Parameter …

Nach ein paar Fehlschlägen stellte sich heraus: Dieses „altmodische“ Setup ist am besten kontrollierbar.

SSH-Schlüssel-Konfiguration: nicht hardcoden

Die häufigste Frage: Wo kommt der SSH-Schlüssel hin?

Anfänger schreiben den privaten Schlüssel direkt in die Workflow-Datei. Ein großer Fehler. GitHub Secrets ist der richtige Ort.

Unter Repository Settings → Secrets → Actions SSH_PRIVATE_KEY anlegen. Im Workflow so verwenden:

- name: Setup SSH
  uses: webfactory/[email protected]
  with:
    ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

Diese Action startet ssh-agent und lädt Ihren Schlüssel automatisch.

known_hosts: „Host key verification failed“ vermeiden

Beim ersten SSH-Verbindungsaufbau fragt der Client, ob Sie dem Host vertrauen. Interaktive Abfragen funktionieren in CI nicht – der Server-Fingerprint muss vorab in known_hosts stehen.

Zwei Varianten:

Variante 1: per Action automatisch hinzufügen

- name: Add server to known hosts
  uses: webfactory/[email protected]
  with:
    ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
    known-hosts: ${{ secrets.SSH_KNOWN_HOSTS }}

Den Inhalt von SSH_KNOWN_HOSTS so ermitteln:

ssh-keyscan -H your-server.com >> known_hosts.txt
# Dateiinhalt in GitHub Secrets kopieren

Variante 2: manuell konfigurieren

- name: Add server to known hosts
  run: |
    mkdir -p ~/.ssh
    ssh-keyscan -H ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts

Variante 1 ist sauberer; Variante 2 eignet sich zum schnellen Debuggen.

rsync oder scp?

Für Dateitransfers nutze ich rsync. Gründe:

  • Nur geänderte Dateien werden übertragen – spart Zeit
  • Bestimmte Verzeichnisse lassen sich ausschließen (z. B. node_modules)
  • Inkrementelle Synchronisation möglich

Typischer rsync-Befehl:

- name: Deploy to server
  run: |
    rsync -avz --delete \
      --exclude 'node_modules' \
      --exclude '.git' \
      ./dist/ ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}:/var/www/html/

--delete entfernt Dateien im Ziel, die in der Quelle fehlen. Vorsicht – bei falschem Pfad können unerwünschte Dateien verschwinden.

Befehle nach dem Deployment: Service neu starten

Statische Websites sind nach dem Upload fertig. Bei Node.js-Anwendungen muss der Dienst neu gestartet werden.

Ich verwalte Node-Prozesse mit PM2. Nach dem Deployment:

- name: Restart application
  run: |
    ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} \
      "cd /var/www/app && pm2 restart all"

Oder gezielter – nur eine Anwendung:

- name: Restart application
  run: |
    ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} \
      "pm2 restart my-app --update-env"

--update-env lädt Umgebungsvariablen neu – sinnvoll bei Konfigurationsänderungen.


Cloud-Deployment: Komfort durch Managed Services

Beim VPS-Deployment verwalten Sie den Server selbst. Sicherheitspatches, SSL-Zertifikatsverlängerung, Firewall-Regeln … viel Nebenarbeit.

Hosting-Plattformen sind unkomplizierter: Code pushen, automatisch bauen und deployen. Sie konzentrieren sich auf den Code.

Vercel: erste Wahl für Frontend-Projekte

Vercel unterstützt Frontend-Projekte nahezu perfekt. Next.js, Astro, React – One-Click-Deployment, null Konfiguration.

Braucht Ihr Projekt Backend-APIs, beachten Sie die Limits: Serverless Functions haben Laufzeitbeschränkungen (Free-Tier 10 Sekunden, Pro 60 Sekunden). Überschreitung führt zu Timeout.

Für statische Sites oder einfache APIs reicht Vercel. Komplexe Backend-Dienste erfordern eigene Infrastruktur.

GitHub Actions Deployment zu Vercel:

name: Deploy to Vercel

on:
  push:
    branches: [main]

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

      - name: Install Vercel CLI
        run: npm i -g vercel@latest

      - name: Pull Vercel Environment Information
        run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}

      - name: Build Project Artifacts
        run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}

      - name: Deploy Project Artifacts to Vercel
        run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}

VERCEL_TOKEN in der Vercel-Konsole erzeugen und als GitHub Secret speichern.

Cloudflare Pages: großzügiges Free-Tier

Cloudflare Pages bietet mehr als Vercel im Free-Tier: unbegrenzte Bandbreite, 500 Builds pro Monat – für persönliche Projekte mehr als genug.

Das globale CDN ist schnell. In meinen Tests war der Zugriff aus Asien stabiler als bei Vercel.

Deployment-Konfiguration:

name: Deploy to Cloudflare Pages

on:
  push:
    branches: [main]

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

      - name: Build
        run: npm run build

      - name: Deploy
        uses: cloudflare/pages-action@v1
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          projectName: my-project
          directory: dist

Zusätzlicher Vorteil: R2-Speicher mit großzügigem Free-Tier. Statische Assets in R2, CDN über Pages – spürbar schnellere Ladezeiten.

Netlify: etablierter Klassiker

Netlify nutze ich seltener als die beiden anderen, aber die Plattform ist etabliert und das Ökosystem reif.

Ähnliche Deployment-Konfiguration:

- name: Deploy to Netlify
  uses: netlify/actions/cli@master
  with:
    args: deploy --prod
  env:
    NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
    NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}

Netlify Form Handling ist praktisch – Formularübermittlungen werden automatisch verarbeitet, ideal für einfache Landing Pages.

Grenzen von Hosting-Plattformen

Hosting-Plattformen sind nicht allmächtig.

Typische Einschränkungen:

  1. Begrenzte Build-Umgebung: RAM und CPU haben Obergrenzen; große Projekte können fehlschlagen
  2. Geringe Anpassbarkeit: nginx-Konfiguration ändern? Unmöglich
  3. Plattformabhängigkeit: Policy-Änderungen oder Plattform-Ausfall erzwingen Migration
  4. Zugriff aus China: Manche Plattformen sind dort instabil (Cloudflare hat sich verbessert)

Brauchen Sie volle Kontrolle, führt kein Weg am VPS vorbei.


Hybrid-Strategie: Flexibilität und Kontrolle

Viele Projekte sind weder rein statisch noch rein Backend. Frontend in Next.js, Backend mit Datenbank und Cronjobs …

Dann ist Hybrid-Deployment oft die beste Lösung.

Statische Seiten hosten + API auf VPS

Typische Architektur:

  • Statische Seiten (HTML/CSS/JS) auf Cloudflare Pages oder Vercel
  • Node.js-API auf eigenem VPS
  • Datenbank auf dem VPS (oder Supabase/PlanetScale als Managed Service)

Vorteile:

  • Frontend profitiert von CDN und automatischem HTTPS
  • Backend mit voller Kontrolle, ohne Plattform-Limits
  • Niedrige DB-Latenz (API und Datenbank auf derselben Maschine)

Multi-Stage-Deployment in GitHub Actions

Ein Workflow deployt gleichzeitig an zwei Ziele:

name: Hybrid Deploy

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      artifact-path: ./dist
    steps:
      - uses: actions/checkout@v4
      - name: Install dependencies
        run: npm ci
      - name: Build
        run: npm run build
      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist

  deploy-frontend:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Download artifact
        uses: actions/download-artifact@v4
        with:
          name: build-output
          path: dist
      - name: Deploy to Cloudflare Pages
        uses: cloudflare/pages-action@v1
        with:
          apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
          accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
          projectName: my-frontend
          directory: dist

  deploy-backend:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup SSH
        uses: webfactory/[email protected]
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
      - name: Deploy API to VPS
        run: |
          rsync -avz --delete \
            --exclude 'node_modules' \
            ./api/ ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}:/var/www/api/
      - name: Restart API service
        run: |
          ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} \
            "cd /var/www/api && npm install && pm2 restart api"

Dieser Workflow hat drei Jobs:

  1. build: Projekt bauen, statische Dateien erzeugen
  2. deploy-frontend: Statische Dateien zu Cloudflare Pages deployen
  3. deploy-backend: API auf VPS deployen und Dienst neu starten

needs: build stellt sicher, dass Deploy-Jobs erst nach dem Build laufen. upload-artifact und download-artifact übergeben Build-Artefakte zwischen Jobs.

Getrennte Umgebungsvariablen

Bei Hybrid-Deployment unterscheiden sich Frontend- und Backend-Umgebungsvariablen.

Das Frontend braucht die API-URL; das Backend Datenbankpasswörter.

Mein Ansatz:

# Frontend-Job
- name: Set frontend env
  run: |
    echo "API_URL=https://api.mydomain.com" >> $GITHUB_ENV

# Backend-Job
- name: Deploy with env
  run: |
    ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} \
      "cd /var/www/api && pm2 restart api --update-env DATABASE_URL=${{ secrets.DATABASE_URL }}"

Sensible Daten (DB-Passwörter, API-Tokens) immer über GitHub Secrets. Nicht-sensible Werte (API-URL) können im Workflow stehen.


Praxis-Konfiguration

Unten ein vollständiger VPS-Deployment-Workflow, der alle genannten Punkte abdeckt.

Vollständige Workflow-Datei

name: Deploy to VPS

on:
  push:
    branches: [main]
  workflow_dispatch:  # Manuelles Deployment

env:
  NODE_VERSION: '20'

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

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm test

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

      - name: Setup Node.js
        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: Upload build artifact
        uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist
          retention-days: 1

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Download build artifact
        uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist

      - name: Setup SSH
        uses: webfactory/[email protected]
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

      - name: Add server to known hosts
        run: |
          mkdir -p ~/.ssh
          ssh-keyscan -H ${{ secrets.SERVER_HOST }} >> ~/.ssh/known_hosts

      - name: Deploy files
        run: |
          rsync -avz --delete \
            --exclude '.htaccess' \
            ./dist/ ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}:${{ secrets.DEPLOY_PATH }}

      - name: Verify deployment
        run: |
          ssh ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} \
            "ls -la ${{ secrets.DEPLOY_PATH }}"

      - name: Send deployment notification
        if: always()
        run: |
          curl -X POST "${{ secrets.NOTIFICATION_WEBHOOK }}" \
            -H "Content-Type: application/json" \
            -d '{"text": "Deployment completed: ${GITHUB_SHA}"}'

Erforderliche Secrets

Secret-NameBeschreibungSo ermitteln
SSH_PRIVATE_KEYPrivater SSH-SchlüsselLokal erzeugen, öffentlichen Schlüssel auf Server
SERVER_HOSTServer-IP oder DomainIhre VPS-Informationen
SERVER_USERSSH-BenutzernameMeist root oder ubuntu
DEPLOY_PATHZiel-Deployment-Pfadz. B. /var/www/html
NOTIFICATION_WEBHOOKDeployment-BenachrichtigungSlack/Telegram-Webhook

Häufige Probleme beheben

Bei fehlgeschlagenen Deployments überwältigen Logs schnell.

Meine Prüfreihenfolge:

  1. SSH-Verbindung: Schritte „Setup SSH“ und „Add server to known hosts“
    • Bei Fehlern Schlüsselformat und known_hosts-Inhalt prüfen
  2. rsync-Transfer: Schritt „Deploy files“
    • Bei Fehlern Pfad-Existenz und Berechtigungen prüfen
  3. Service-Neustart: Schritt „Verify deployment“
    • Bei Fehlern prüfen, ob Dateien im Zielpfad vorhanden sind

Tipp: Debug-Ausgabe nach dem fehlgeschlagenen Schritt ergänzen.

- name: Debug SSH connection
  if: failure()
  run: |
    echo "SSH config:"
    cat ~/.ssh/config || echo "No config file"
    echo "Known hosts:"
    cat ~/.ssh/known_hosts || echo "No known_hosts file"
    ssh -v ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} echo "Connection test"

ssh -v liefert ausführliche Logs und zeigt, wo das Problem liegt.


Fazit

Kurz gesagt: Es gibt keine perfekte Deployment-Lösung – nur die passende für Ihr Projekt.

Auswahl-Empfehlungen:

  • Reine statische Sites (Blog, Docs): Cloudflare Pages oder Vercel – unkompliziert
  • Einfache API + Frontend: Hosting-Plattform reicht, VPS vermeiden
  • Komplexes Backend + Datenbank: VPS oder Cloud-Server – Kontrolle zählt
  • Hybrid-Architektur: Frontend gehostet + Backend auf VPS – das Beste aus beiden Welten

Unabhängig von der Wahl folgen GitHub Actions Workflows einem Muster: Build → Transfer → Neustart. Diese drei Schritte klar trennen, und Debugging wird übersichtlich.

Bei Deployment-Fehlern: ruhig bleiben. Logs segmentweise lesen – zuerst SSH oder Befehlsausführung isolieren. Ein Debug-Schritt deckt Probleme meist schnell auf.

Beim nächsten Deployment um drei Uhr morgens finden Sie hoffentlich schneller die Ursache.

GitHub Actions VPS-Deployment konfigurieren

Vollständige Einrichtung eines GitHub Actions Workflows für SSH-Deployment auf einen VPS

⏱️ Estimated time: 30 min

  1. 1

    Step 1: SSH-Schlüsselpaar erzeugen

    Lokal einen dedizierten SSH-Schlüssel für Deployments erzeugen:

    • ssh-keygen -t ed25519 -C "deploy@github" -f deploy_key
    • Öffentlichen Schlüssel (deploy_key.pub) in ~/.ssh/authorized_keys auf dem Server eintragen
    • Privaten Schlüssel (deploy_key) als GitHub Secret SSH_PRIVATE_KEY speichern
  2. 2

    Step 2: GitHub Secrets konfigurieren

    Unter Repository Settings → Secrets → Actions hinzufügen:

    • SSH_PRIVATE_KEY: vollständiger privater Schlüssel
    • SERVER_HOST: Server-IP oder Domain
    • SERVER_USER: SSH-Benutzername (z. B. root oder ubuntu)
    • DEPLOY_PATH: Ziel-Deployment-Pfad
  3. 3

    Step 3: Workflow-Datei anlegen

    Unter .github/workflows/deploy.yml die Deployment-Konfiguration erstellen:

    • SSH-Schlüssel-Schritt hinzufügen (webfactory/ssh-agent-action)
    • known_hosts konfigurieren, um Host-Verification-Fehler zu vermeiden
    • rsync für Build-Artefakte nutzen
    • Nach dem Deployment Service-Neustart ausführen
  4. 4

    Step 4: Deployment-Prozess testen

    Code pushen, um automatisches Deployment auszulösen, oder manuell triggern:

    • Log-Ausgabe jedes Schritts beobachten
    • Bei SSH-Fehlern Schlüsselformat und known_hosts prüfen
    • Bei rsync-Fehlern Pfade und Berechtigungen kontrollieren
    • Debug-Schritte zur Fehlersuche ergänzen

FAQ

Wie behebe ich ‚Host key verification failed‘ bei GitHub Actions Deployment?
Das passiert, wenn beim ersten SSH-Verbindungsaufbau known_hosts fehlt. Zwei Lösungen:

• Variante 1: Server-Fingerprint per ssh-keyscan holen und als SSH_KNOWN_HOSTS Secret speichern
• Variante 2: In der Workflow-Datei manuell ssh-keyscan -H $SERVER_IP >> ~/.ssh/known_hosts ausführen

Variante 1 ist sauberer und sicherer.
Wo gehört der SSH-Schlüssel hin? Direkt in die Workflow-Datei?
Auf keinen Fall. Private Schlüssel gehören in GitHub Secrets; der Workflow referenziert sie über {{ secrets.SSH_PRIVATE_KEY }}. Hardcodierte Schlüssel landen im Repository und sind für jeden sichtbar – ein schwerwiegendes Sicherheitsrisiko.
Vercel, Cloudflare Pages oder Netlify – was eignet sich für persönliche Projekte?
Cloudflare Pages bietet das großzügigste Free-Tier (unbegrenzte Bandbreite, 500 Builds/Monat) und stabile Zugriffszeiten in Asien. Vercel ist für Next.js-Projekte am angenehmsten, hat aber ein 10-Sekunden-Limit für Serverless Functions im Free-Tier. Netlify hat ein reifes Ökosystem; Form Handling ist praktisch.

Für reine statische Sites: Cloudflare Pages zuerst prüfen.
Welche Vorteile hat eine Hybrid-Deployment-Architektur?
Frontend auf einer Hosting-Plattform mit CDN und automatischem HTTPS; Backend auf dem VPS mit voller Kontrolle ohne Plattform-Limits. Ideal für Projekte mit Datenbank, Cronjobs oder komplexen Backend-Diensten.

Mit Multi-Job-Workflows in GitHub Actions deployen Sie gleichzeitig an beide Ziele.
Bei Deployment-Fehlern ist das Log unübersichtlich – wie finde ich schnell die Ursache?
Logs segmentweise lesen und der Reihe nach prüfen:

1. SSH-Verbindung → Setup SSH und known_hosts-Schritte
2. rsync-Transfer → Pfad-Existenz und Berechtigungen
3. Service-Neustart → Dateiliste im Zielpfad

Debug-Ausgaben nach dem fehlgeschlagenen Schritt (ssh -v) decken Probleme schnell auf.
Welche Risiken hat der rsync-Parameter --delete?
--delete entfernt Dateien im Zielverzeichnis, die in der Quelle fehlen – für vollständige Synchronisation. Bei falschem Pfad können aber auch unerwünschte Dateien gelöscht werden.

Beim ersten Deployment --delete weglassen; nach Bestätigung aktivieren. Alternativ --delete-excluded, um nur ausgeschlossene Dateien zu entfernen.

8 Min. Lesezeit · Veröffentlicht am: 7. Apr. 2026 · Aktualisiert am: 9. Juli 2026

Kommentare

Melde dich mit GitHub an, um einen Kommentar zu hinterlassen

Easton BlogEaston Blog