Zurück zum Blog
DevOps19. Dezember 2024Michael Hettwer12 min read

Zero-Downtime-Bereitstellungen mit Nginx und Systemd

Eine Schritt-für-Schritt-Anleitung zur Erreichung echter Zero-Downtime-Releases mit Nginx-Upstream-Switching und systemd-Socket-Aktivierung — kein Kubernetes erforderlich.

Zero-Downtime-Deployments werden oft als Kubernetes-only-Fähigkeit präsentiert. Sie sind es nicht. Mit Nginx als Reverse-Proxy und systemd Socket-Activation können Sie echte Zero-Downtime-Anwendungs-Updates auf einem einzelnen Server erreichen, ohne Container-Orchestrator erforderlich — und genau verstehen, was bei jedem Schritt passiert.

Die Grundidee

Die Strategie hat zwei Teile: (1) führen Sie zwei Versionen Ihrer Anwendung gleichzeitig für eine kurze Periode während der Bereitstellung aus, und (2) verwenden Sie Nginx, um Traffic atomisch von der alten zur neuen Version zu verschieben. Systemd Socket-Activation stellt sicher, dass neue Verbindungen gehalten — nicht abgebrochen — während des Switchovers werden.

Schritt 1: systemd Socket-Aktivierung

Socket-Activation lässt systemd den Listening-Socket besitzen. Wenn Ihre Anwendung neu startet, bleibt der Socket offen — eingehende Verbindungen warten in der Kernel-Queue — und werden dem neuen Prozess übergeben, sobald er bereit ist. Keine Verbindungen werden abgelehnt.

ini
# /etc/systemd/system/myapp.socket
[Unit]
Description=MyApp socket

[Socket]
ListenStream=127.0.0.1:3000
Accept=no

[Install]
WantedBy=sockets.target
ini
# /etc/systemd/system/myapp.service
[Unit]
Description=MyApp application server
Requires=myapp.socket
After=myapp.socket

[Service]
ExecStart=/usr/bin/node /srv/myapp/current/server.js
WorkingDirectory=/srv/myapp/current
User=myapp
Group=myapp
Restart=on-failure

# Dem Service mitteilen, dass er den von systemd übergebenen Socket verwendet
Environment=NODE_ENV=production

[Install]
WantedBy=multi-user.target

Schritt 2: Nginx-Upstream-Konfiguration

nginx
# /etc/nginx/conf.d/myapp.conf
upstream myapp {
    server 127.0.0.1:3000;
    keepalive 32;
}

server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://myapp;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # Nginx darf Verbindungen während des Neustarts kurz halten
        proxy_next_upstream error timeout;
        proxy_connect_timeout 5s;
        proxy_read_timeout 60s;
    }
}

Schritt 3: Das Deployment-Skript

bash
#!/bin/bash
# deploy.sh — Zero-Downtime-Deployment
set -euo pipefail

APP_DIR=/srv/myapp
RELEASE=$(date +%Y%m%d%H%M%S)
RELEASE_DIR="$APP_DIR/releases/$RELEASE"

echo "==> Release-Verzeichnis erstellen: $RELEASE_DIR"
mkdir -p "$RELEASE_DIR"

echo "==> Aktuellen Code ziehen"
git clone --depth 1 git@github.com:yourorg/myapp.git "$RELEASE_DIR"

echo "==> Abhängigkeiten installieren"
cd "$RELEASE_DIR"
npm ci --production

echo "==> Datenbank-Migrationen ausführen"
npm run migrate

echo "==> Symlink atomar umschalten"
ln -sfn "$RELEASE_DIR" "$APP_DIR/current"

echo "==> Anwendung neu laden (Socket bleibt offen)"
# systemd startet den Service neu und hält den Socket offen
systemctl reload-or-restart myapp.service

echo "==> Health-Check verifizieren"
sleep 2
curl -sf http://127.0.0.1:3000/health || { echo "Health-Check fehlgeschlagen!"; exit 1; }

echo "==> Alte Releases aufräumen (letzte 5 behalten)"
ls -dt "$APP_DIR/releases"/* | tail -n +6 | xargs rm -rf

echo "==> Deployment abgeschlossen: $RELEASE"
Tipp:

Der Schlüssel ist ln -sfn, das den Symlink atomisch ersetzt. Ab dem Moment, in dem der Symlink ändert, werden alle neuen Worker-Prozesse, die von systemd gestartet werden, den neuen Code servieren. Bestehende In-Flight-Requests fahren auf den alten Workern fort, bis sie fertig sind.

Schritt 4: Zero Downtime prüfen

bash
# In einem zweiten Terminal während des Deployments ausführen
# Meldet sofort, wenn eine Anfrage fehlschlägt
while true; do
  STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://example.com/health)
  echo "$(date +%H:%M:%S) — HTTP $STATUS"
  sleep 0.2
done

Sie sollten einen kontinuierlichen Strom von "HTTP 200"-Zeilen sehen — einschließlich durch die Bereitstellung. Wenn Sie 502 oder 503 sehen, überprüfen Sie Ihre proxy_next_upstream-Einstellungen und stellen Sie sicher, dass Ihre Anwendung schnell Verbindungen akzeptiert (innerhalb des proxy_connect_timeout-Fensters).

Wann sich Kubernetes lohnt

Dieser Ansatz funktioniert hervorragend für Teams, die einen bis eine Handvoll Server mit einer einzelnen Anwendung betreiben. Wenn Sie Multi-Node-Deployments, automatische horizontale Skalierung oder komplexe Service-Meshes benötigen, verdient Kubernetes seinen Komplexitätskost. Bis dahin ist Nginx + systemd einfacher, schneller zu debuggen und erfordert keinen Cluster zu warten.