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/ssh-agent-action@v0.7.0
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/ssh-agent-action@v0.7.0
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:
- Begrenzte Build-Umgebung: RAM und CPU haben Obergrenzen; große Projekte können fehlschlagen
- Geringe Anpassbarkeit: nginx-Konfiguration ändern? Unmöglich
- Plattformabhängigkeit: Policy-Änderungen oder Plattform-Ausfall erzwingen Migration
- 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/ssh-agent-action@v0.7.0
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:
build: Projekt bauen, statische Dateien erzeugendeploy-frontend: Statische Dateien zu Cloudflare Pages deployendeploy-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/ssh-agent-action@v0.7.0
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-Name | Beschreibung | So ermitteln |
|---|---|---|
SSH_PRIVATE_KEY | Privater SSH-Schlüssel | Lokal erzeugen, öffentlichen Schlüssel auf Server |
SERVER_HOST | Server-IP oder Domain | Ihre VPS-Informationen |
SERVER_USER | SSH-Benutzername | Meist root oder ubuntu |
DEPLOY_PATH | Ziel-Deployment-Pfad | z. B. /var/www/html |
NOTIFICATION_WEBHOOK | Deployment-Benachrichtigung | Slack/Telegram-Webhook |
Häufige Probleme beheben
Bei fehlgeschlagenen Deployments überwältigen Logs schnell.
Meine Prüfreihenfolge:
- SSH-Verbindung: Schritte „Setup SSH“ und „Add server to known hosts“
- Bei Fehlern Schlüsselformat und known_hosts-Inhalt prüfen
- rsync-Transfer: Schritt „Deploy files“
- Bei Fehlern Pfad-Existenz und Berechtigungen prüfen
- 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
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
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
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
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?
• 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?
Vercel, Cloudflare Pages oder Netlify – was eignet sich für persönliche Projekte?
Für reine statische Sites: Cloudflare Pages zuerst prüfen.
Welche Vorteile hat eine Hybrid-Deployment-Architektur?
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?
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?
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
GitHub Actions Komplettleitfaden
Wenn du über die Suche hier gelandet bist, kommst du am schnellsten weiter, indem du zum vorherigen oder nächsten Beitrag dieser Serie springst.
Vorheriger
GitHub Actions Matrix-Build: Parallele Tests auf mehreren Plattformen und Versionen
Praxisleitfaden zu GitHub Actions Matrix: Basis-Syntax, exclude/include, fail-fast, max-parallel und dynamische Matrix – 5 produktionsreife Workflow-Vorlagen, 60 %+ weniger Konfigurationscode.
Teil 5 von 10
Nächster
GitHub Actions Secrets verwalten: Von Leckrisiken zu OIDC ohne statische Credentials
Leitfaden zu GitHub Actions Secrets: Drei-Ebenen-Architektur, 8 Sicherheitsregeln, OIDC-Deployment ohne Secrets und Schutz vor Supply-Chain-Angriffen. Lehren aus dem tj-actions-Vorfall mit Workflow-YAML-Beispielen und Best Practices.
Teil 7 von 10
Ähnliche Beiträge
GitHub Actions Einstieg: YAML-Workflow-Grundlagen und Trigger-Konfiguration

GitHub Actions Einstieg: YAML-Workflow-Grundlagen und Trigger-Konfiguration
GitHub Actions CI-Pipeline in der Praxis: Build und Test automatisieren


Kommentare
Melde dich mit GitHub an, um einen Kommentar zu hinterlassen