Manuelle Deployments sind eine Quelle von Fehlern und Zeitverschwendung. Jeder kennt den Ablauf: SSH einloggen, git pull, Services neustarten, vergessen dass noch pip install nötig war, nochmal einloggen... Irgendwann hatte ich genug davon und habe mir einen einfachen Webhook-Mechanismus gebaut, der das alles automatisch erledigt – ausgelöst durch einen git push.

Das Schöne daran: Es braucht keine externe CI/CD-Plattform, keine GitHub Actions, kein Jenkins. Ein kleines Go-Binary auf dem Server und ein paar Bash-Scripts reichen vollständig aus.

Das Prinzip

GitHub kann bei jedem Push an einen Branch eine HTTP-Anfrage an eine beliebige URL schicken – den sogenannten Webhook. Diese Anfrage enthält Informationen über den Push (Branch, Commit, Repository). Ein kleiner Dienst auf dem Server empfängt diese Anfrage, validiert sie kryptografisch und führt ein Deploy-Script aus.

git push origin main
    ↓
GitHub sendet POST an https://example.com/webhook/<geheimer-pfad>
    ↓
webhook-receiver validiert HMAC-SHA256-Signatur
    ↓
deploy-script.sh wird ausgeführt
    ↓
App ist aktualisiert

Der webhook-receiver Dienst

Als Webhook-Empfänger verwende ich adnanh/webhook – ein schlankes Go-Binary, das als systemd-Dienst läuft. Die Konfiguration erfolgt über eine JSON-Datei:

[
  {
    "id": "deploy-meinprojekt",
    "execute-command": "/etc/webhook-scripts/deploy.sh",
    "trigger-rule": {
      "and": [
        {
          "match": {
            "type": "payload-hmac-sha256",
            "secret": "LANGER_ZUFAELLIGER_TOKEN",
            "parameter": { "source": "header", "name": "X-Hub-Signature-256" }
          }
        },
        {
          "match": {
            "type": "value",
            "value": "refs/heads/main",
            "parameter": { "source": "payload", "name": "ref" }
          }
        }
      ]
    }
  }
]

Zwei Dinge sind hier entscheidend: Die HMAC-Signatur stellt sicher, dass nur echte GitHub-Webhooks den Deploy auslösen – kein fremder Request kann ohne das Secret eine gültige Signatur erzeugen. Der Branch-Filter stellt sicher, dass nur Pushes auf den richtigen Branch den Produktions-Deploy auslösen.

Das Deploy-Script

#!/bin/bash
set -e

cd /var/www/meinprojekt
git pull origin main
venv/bin/pip install -r requirements.txt --quiet
DJANGO_STATIC_ROOT=/var/lib/meinprojekt/staticfiles   venv/bin/python manage.py collectstatic --noinput
chown -R meinprojekt:meinprojekt /var/lib/meinprojekt/staticfiles/
systemctl restart meinprojekt
echo "[$(date)] Deploy erfolgreich" >> /var/log/webhook-deploy.log

sudo ohne Passwort – aber sicher

Der Webhook-Dienst läuft unter einem eigenen unprivilegierten User. Er braucht aber Rechte, um systemd-Dienste neuzustarten. Dafür reicht eine gezielte sudo-Regel – kein NOPASSWD: ALL, nur die spezifischen Scripts:

security.sudo.extraRules = [{
  users = [ "webhook" ];
  commands = map (s: { command = s; options = [ "NOPASSWD" ]; }) [
    "/etc/webhook-scripts/deploy-app1.sh"
    "/etc/webhook-scripts/deploy-app2.sh"
  ];
}];

Ergebnis

Seit ich dieses Setup betreibe, besteht ein Deployment aus einem einzigen Befehl: git push. Durchschnittlich 15 Sekunden später ist die App auf dem neuesten Stand. Das Setup skaliert problemlos auf mehrere Apps – jede bekommt ein eigenes Deploy-Script und einen eigenen Webhook-Hook. Auf meinem Server deployen sich so aktuell sechs verschiedene Apps vollautomatisch.