GitHub Actions CI/CD: Zero-Downtime-Deployment Guide
Automatisches Deployment mit GitHub Actions einrichten: CI/CD-Pipeline für Zero-Downtime-Deployments auf deinem VPS – Schritt für Schritt erklärt.
Wer schon einmal eine Anwendung manuell per SSH deployt hat, kennt das Problem: Der Prozess ist fehleranfällig, zeitintensiv und sorgt im schlimmsten Fall für Ausfallzeiten. Mit GitHub Actions CI/CD lässt sich dieser Workflow vollständig automatisieren – inklusive Zero-Downtime-Deployment. In diesem Guide zeige ich dir Schritt für Schritt, wie du eine produktionsreife Pipeline aufsetzt, die deinen Code bei jedem Push automatisch testet, baut und ohne Unterbrechung auf deinen VPS deployt.
Warum GitHub Actions für CI/CD?
GitHub Actions ist direkt in GitHub integriert und benötigt keinen externen CI/CD-Server wie Jenkins oder GitLab Runner. Das bringt mehrere Vorteile:
- Keine zusätzliche Infrastruktur – die Pipeline läuft auf GitHub-hosted Runnern
- YAML-basierte Konfiguration direkt im Repository
- Marketplace mit tausenden Actions für gängige Aufgaben
- Kostenlos für öffentliche Repositories und mit großzügigem Free-Tier für private Repos
Für ein Zero-Downtime-Deployment kombinieren wir GitHub Actions mit einer Blue-Green-Deployment-Strategie auf dem VPS. So bleibt deine Anwendung während des gesamten Deployment-Prozesses erreichbar.
Voraussetzungen
Bevor wir starten, brauchst du folgende Dinge:
- Einen VPS mit Ubuntu 22.04 oder neuer (z. B. bei Hetzner, Netcup oder DigitalOcean)
- Docker und Docker Compose auf dem Server installiert
- Ein GitHub-Repository mit deiner Anwendung
- SSH-Zugang zum Server mit Key-basierter Authentifizierung
- Nginx als Reverse Proxy auf dem Server
SSH-Key für GitHub Actions erstellen
Erstelle auf deinem lokalen Rechner einen dedizierten SSH-Key für das Deployment:
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/github_actions_deploy
Kopiere den öffentlichen Schlüssel auf deinen Server:
ssh-copy-id -i ~/.ssh/github_actions_deploy.pub [email protected]
Den privaten Schlüssel brauchst du gleich als GitHub Secret.
GitHub Secrets einrichten
Navigiere in deinem Repository zu Settings → Secrets and variables → Actions und lege folgende Secrets an:
SSH_PRIVATE_KEY– Der Inhalt deiner privaten Key-DateiSSH_HOST– Die IP-Adresse oder Domain deines ServersSSH_USER– Der SSH-BenutzernameSSH_PORT– Der SSH-Port (Standard: 22)
Die Projektstruktur vorbereiten
Unsere Beispiel-Anwendung ist eine Node.js-App mit Docker. Die relevante Struktur sieht so aus:
├── .github/
│ └── workflows/
│ └── deploy.yml
├── docker-compose.blue.yml
├── docker-compose.green.yml
├── Dockerfile
├── nginx/
│ └── app.conf
├── deploy.sh
└── src/
└── index.js
Dockerfile erstellen
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY src/ ./src/
COPY package.json ./
EXPOSE 3000
USER node
CMD ["node", "src/index.js"]
Docker Compose für Blue-Green-Deployment
Die Blue-Green-Strategie nutzt zwei identische Umgebungen. Während eine aktiv ist, wird die andere aktualisiert.
# docker-compose.blue.yml
services:
app-blue:
build: .
container_name: app-blue
restart: unless-stopped
ports:
- "3001:3000"
environment:
- NODE_ENV=production
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
interval: 10s
timeout: 5s
retries: 3
# docker-compose.green.yml
services:
app-green:
build: .
container_name: app-green
restart: unless-stopped
ports:
- "3002:3000"
environment:
- NODE_ENV=production
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
interval: 10s
timeout: 5s
retries: 3
Das Deploy-Script
Das Deploy-Script ist das Herzstück des Zero-Downtime-Deployments. Es erkennt die aktive Umgebung, startet die inaktive mit dem neuen Code, prüft die Gesundheit und schaltet den Traffic um.
#!/bin/bash
set -euo pipefail
APP_DIR="/opt/app"
NGINX_CONF="/etc/nginx/sites-available/app.conf"
cd "$APP_DIR"
# Aktive Umgebung erkennen
if docker ps --format '{{.Names}}' | grep -q "app-blue"; then
ACTIVE="blue"
INACTIVE="green"
NEW_PORT="3002"
else
ACTIVE="green"
INACTIVE="blue"
NEW_PORT="3001"
fi
echo "Aktiv: $ACTIVE → Deploye auf: $INACTIVE"
# Neue Version bauen und starten
docker compose -f "docker-compose.${INACTIVE}.yml" build --no-cache
docker compose -f "docker-compose.${INACTIVE}.yml" up -d
# Health-Check: Warte bis der neue Container bereit ist
echo "Warte auf Health-Check..."
RETRIES=30
until curl -sf "http://localhost:${NEW_PORT}/health" > /dev/null 2>&1; do
RETRIES=$((RETRIES - 1))
if [ "$RETRIES" -le 0 ]; then
echo "FEHLER: Health-Check fehlgeschlagen. Rollback!"
docker compose -f "docker-compose.${INACTIVE}.yml" down
exit 1
fi
sleep 2
done
echo "Health-Check erfolgreich. Schalte Nginx um..."
# Nginx auf neuen Port umschalten
sudo sed -i "s/localhost:[0-9]*/localhost:${NEW_PORT}/" "$NGINX_CONF"
sudo nginx -t && sudo systemctl reload nginx
# Alten Container stoppen
docker compose -f "docker-compose.${ACTIVE}.yml" down
echo "Deployment abgeschlossen: $INACTIVE ist jetzt aktiv."
Mache das Script ausführbar und lege es auf dem Server ab:
chmod +x deploy.sh
Nginx als Reverse Proxy konfigurieren
Die Nginx-Konfiguration leitet den Traffic an den jeweils aktiven Container weiter:
server {
listen 80;
server_name deine-domain.de;
location / {
proxy_pass http://localhost:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
Die GitHub Actions Workflow-Datei
Jetzt kommt der zentrale Teil: die GitHub Actions CI/CD-Pipeline. Erstelle die Datei .github/workflows/deploy.yml:
name: CI/CD Pipeline
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Repository auschecken
uses: actions/checkout@v4
- name: Node.js einrichten
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Abhängigkeiten installieren
run: npm ci
- name: Linting ausführen
run: npm run lint --if-present
- name: Tests ausführen
run: npm test
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment: production
steps:
- name: Repository auschecken
uses: actions/checkout@v4
- name: SSH-Key einrichten
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -p ${{ secrets.SSH_PORT }} ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts
- name: Code auf Server synchronisieren
run: |
rsync -avz --delete \
-e "ssh -i ~/.ssh/deploy_key -p ${{ secrets.SSH_PORT }}" \
--exclude='.git' \
--exclude='node_modules' \
./ ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/opt/app/
- name: Zero-Downtime-Deployment ausführen
run: |
ssh -i ~/.ssh/deploy_key \
-p ${{ secrets.SSH_PORT }} \
${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} \
"cd /opt/app && bash deploy.sh"
- name: Deployment verifizieren
run: |
sleep 5
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://deine-domain.de/health)
if [ "$STATUS" != "200" ]; then
echo "Deployment-Verifikation fehlgeschlagen! Status: $STATUS"
exit 1
fi
echo "Deployment erfolgreich verifiziert."
- name: Aufräumen
if: always()
run: rm -f ~/.ssh/deploy_key
Workflow im Detail
Die Pipeline besteht aus zwei Jobs:
- test – Installiert Abhängigkeiten, führt Linting und Tests aus. Schlägt ein Test fehl, wird das Deployment gar nicht erst gestartet.
- deploy – Synchronisiert den Code per
rsyncauf den Server und führt das Deploy-Script aus. Anschließend wird per Health-Check verifiziert, dass die neue Version erreichbar ist.
Das Schlüsselwort needs: test stellt sicher, dass der Deploy-Job erst nach erfolgreichem Test startet. Mit environment: production kannst du in GitHub zusätzlich manuelle Approvals konfigurieren.
Automatisches Rollback absichern
Das Deploy-Script enthält bereits einen eingebauten Rollback-Mechanismus: Wenn der Health-Check der neuen Version fehlschlägt, wird der neue Container sofort gestoppt und die alte Version bleibt aktiv. Für zusätzliche Sicherheit kannst du eine Benachrichtigung einbauen:
- name: Slack-Benachrichtigung bei Fehler
if: failure()
uses: slackapi/slack-github-action@v1
with:
payload: |
{"text": "⚠️ Deployment fehlgeschlagen! Repository: ${{ github.repository }}, Commit: ${{ github.sha }}"}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
Tipps für die Praxis
- Branch-Protection aktivieren: Stelle sicher, dass direkte Pushes auf
mainblockiert sind und nur gemergte Pull Requests ein Deployment auslösen. - Caching nutzen: Docker-Layer-Caching beschleunigt den Build-Prozess erheblich. Nutze
docker build --cache-fromoder GitHub Actions Cache. - Secrets rotieren: Wechsle SSH-Keys und andere Credentials regelmäßig.
- Monitoring einrichten: Kombiniere dein CI/CD-Setup mit Uptime-Monitoring (z. B. Uptime Kuma), um Probleme sofort zu erkennen.
- Logs aufbewahren: Leite die Container-Logs an einen zentralen Logging-Dienst weiter, um Fehler nach einem Deployment schnell analysieren zu können.
Fazit
Mit GitHub Actions CI/CD und einer Blue-Green-Deployment-Strategie erreichst du vollautomatische, sichere Deployments ohne Ausfallzeiten. Die gesamte Pipeline – vom Testen über das Bauen bis zum Zero-Downtime-Deployment – läuft bei jedem Push auf den Main-Branch ab, ohne dass du manuell eingreifen musst.
Der wichtigste Vorteil: Durch den integrierten Health-Check und den automatischen Rollback minimierst du das Risiko fehlerhafter Releases drastisch. Selbst wenn ein Deployment schiefgeht, bleibt die alte Version aktiv und deine Nutzer merken nichts davon.
Starte am besten mit einer einfachen Anwendung, teste die Pipeline gründlich in einer Staging-Umgebung und rolle sie dann schrittweise für deine Produktionssysteme aus. Einmal aufgesetzt, wirst du dich fragen, warum du jemals manuell deployt hast.