Zum Inhalt springen
Alle Artikel
GitHub ActionsCI/CDZero-Downtime-Deployment

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.

9. März 20267 Min. Lesezeit

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-Datei
  • SSH_HOST – Die IP-Adresse oder Domain deines Servers
  • SSH_USER – Der SSH-Benutzername
  • SSH_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:

  1. test – Installiert Abhängigkeiten, führt Linting und Tests aus. Schlägt ein Test fehl, wird das Deployment gar nicht erst gestartet.
  2. deploy – Synchronisiert den Code per rsync auf 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 main blockiert sind und nur gemergte Pull Requests ein Deployment auslösen.
  • Caching nutzen: Docker-Layer-Caching beschleunigt den Build-Prozess erheblich. Nutze docker build --cache-from oder 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.