Swift Vapor auf einem Cloud-Mac-mini bereitstellen: launchd-Dienste und Zero-Downtime-Releases

DevOps & CI/CD ·ca. 5 Min. Lesezeit

Swift Vapor auf einem Cloud-Mac-mini bereitstellen: launchd-Dienste und Zero-Downtime-Releases

Letzten Monat wurde ein MacBook Pro aus dem Team kurzerhand zum "Not-Backend" erklärt: Darauf lief ein Swift-Vapor-Dienst für die lokale API-Entwicklung einer iOS-App, gestartet per swift run in einem Terminalfenster, das aus Angst vor dem Absturz niemand mehr zu schließen wagte. Dann kam ein automatischer Systemneustart nach einem Update — der Dienst fiel lautlos aus und blieb halben Tag offline. Der QA-Kollege konnte die API nicht erreichen und dachte zunächst, es liege an seinem eigenen Netzwerk. Diese Art von "menschlichem Daemon" muss spätestens dann abgeschafft werden, wenn man sich einen dauerhaft laufenden Cloud-Mac-mini mietet. Dieser Beitrag dokumentiert den vollständigen Weg, einen Vapor-Dienst aus dem Terminal in einen launchd-Dienst zu überführen und anschließend Releases ohne Ausfallzeit auszuliefern.

Szenario: Warum Vapor-Dienste auf einen Cloud-Mac-mini gehören

Swift Vapor wird häufig als "leichtgewichtiges Backend für den iOS-Client" eingesetzt: Mock-Logins, Push-Callbacks, Tracking-Endpunkte, interne Admin-Oberflächen. Solche Dienste sind klein im Umfang, müssen aber rund um die Uhr laufen — und zwar auf einer echten macOS-Umgebung, da manche Szenarien APNs-Zertifikate testen oder native Abhängigkeiten via swift build kompilieren müssen. Ein Cloud-Mac-mini als dediziertes physisches Gerät (keine virtuelle Maschine) schließt exakt diese Lücke: Der lokale Rechner kann nicht dauerhaft eingeschaltet bleiben, und Linux-Cloud-Instanzen können macOS-spezifische Abhängigkeiten nicht ausführen. Sobald man aber eine dedizierte Bare-Metal-Maschine hat, liegt die gesamte Verantwortung für das Prozessmanagement bei einem selbst — es gibt keine Plattform-Schicht, die abgestürzte Prozesse für dich neu startet. launchd muss man sich selbst einrichten.

Umgebung vorbereiten: Toolchain und Portplanung

Nach dem Login zunächst die Toolchain-Versionen prüfen, um Unterschiede zwischen lokaler Entwicklungsumgebung und Cloud-Umgebung zu vermeiden, die sich auf das Verhalten der Build-Artefakte auswirken könnten:

swift --version
xcode-select -p
mkdir -p ~/apps/vapor-api/releases
mkdir -p ~/apps/vapor-api/logs

Bei der Portplanung von Anfang an zwei Ports vorsehen, um später eine Umschaltung ohne Ausfallzeit zu ermöglichen:

Zweck Port Beschreibung
Produktions-Hauptport 8080 Aktuell aktive Version, über Nginx nach außen proxied
Staging-/Neue-Version-Port 8081 Neuer Build läuft hier zunächst zur Selbstprüfung
Interner Health-Check 8080/8081 /healthz Vapor-Route, benutzerdefiniert, liefert die Build-Versionsnummer zurück

Die /healthz-Route sollte direkt den kurzen Git-Commit-Hash zurückgeben. So lässt sich beim Release auf einen Blick prüfen, ob tatsächlich auf die richtige Version umgeschaltet wurde — ohne raten zu müssen, ob der Prozess wirklich neu gestartet ist.

launchd-Dienst statt nohup

Die Datei ~/Library/LaunchAgents/com.m4rent.vaporapi.plist wie folgt anlegen:

<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.m4rent.vaporapi</string>
  <key>ProgramArguments</key>
  <array>
    <string>/Users/deploy/apps/vapor-api/current/Run</string>
    <string>serve</string>
    <string>--hostname</string>
    <string>127.0.0.1</string>
    <string>--port</string>
    <string>8080</string>
  </array>
  <key>WorkingDirectory</key>
  <string>/Users/deploy/apps/vapor-api/current</string>
  <key>KeepAlive</key>
  <true/>
  <key>RunAtLoad</key>
  <true/>
  <key>StandardOutPath</key>
  <string>/Users/deploy/apps/vapor-api/logs/stdout.log</string>
  <key>StandardErrorPath</key>
  <string>/Users/deploy/apps/vapor-api/logs/stderr.log</string>
</dict>
</plist>

Laden und verifizieren:

launchctl load ~/Library/LaunchAgents/com.m4rent.vaporapi.plist
launchctl list | grep vaporapi
curl -s localhost:8080/healthz

Die wichtigsten plist-Felder im Überblick

Ist KeepAlive auf true gesetzt, startet launchd den Prozess bei einem unerwarteten Absturz automatisch neu — ein zusätzliches Watchdog-Skript ist nicht nötig. Vorsicht ist jedoch geboten, wenn die Konfiguration selbst fehlerhaft ist und der Prozess sofort beim Start abstürzt: Dann gerät launchd in eine "Sekunden-Neustart"-Endlosschleife. In diesem Fall hilft ein Blick auf die Neustartzahl in launchctl list schneller weiter als die Logs. RunAtLoad sorgt dafür, dass der Dienst nach Boot bzw. Login automatisch hochfährt — zusammen mit dem geplanten stillstandsfreien Betrieb eines Cloud-Mac-mini muss man sich um den Startvorgang praktisch nicht mehr manuell kümmern. WorkingDirectory sollte immer explizit gesetzt werden, sonst werden relative Pfade für Konfigurationsdateien und Logs falsch aufgelöst, weil das Standard-Arbeitsverzeichnis von launchd nicht dem erwarteten Ort entspricht.

Zero-Downtime-Release: Doppel-Port + Nginx-Reverse-Proxy-Umschaltung

Beim Release einer neuen Version das current-Verzeichnis nicht einfach überschreiben und denselben Prozess neu starten — dabei entsteht zwangsläufig eine Lücke von einigen Sekunden ohne Dienst. Stattdessen: "Neuer Prozess auf neuem Port, erst nach bestätigtem Health-Check den Traffic umschalten":

cp -R releases/build-2026-07-12 releases/build-2026-07-12-verify
sed -i '' 's/8080/8081/' com.m4rent.vaporapi-staging.plist
launchctl load com.m4rent.vaporapi-staging.plist
curl -s localhost:8081/healthz

Sobald /healthz den erwarteten Commit-Hash zurückliefert, den Nginx-Upstream umschalten:

upstream vapor_api {
    server 127.0.0.1:8081;
}

nginx -s reload lädt nur die Konfiguration neu, ohne bestehende Verbindungen zu unterbrechen. Der alte Prozess auf Port 8080 verarbeitet noch offene Requests zu Ende und wird anschließend per launchctl unload sauber heruntergefahren.

Auf keinen Fall den alten Port stoppen, bevor der Traffic umgeschaltet ist — ein erfolgreicher Health-Check bedeutet nicht, dass die neue Version unter echter Last problemlos läuft. Mindestens ein paar Minuten Beobachtungsfenster einplanen; im Fehlerfall lässt sich mit einem einzigen Befehl auf den alten Upstream zurückschalten, das ist deutlich schneller als ein nachträgliches Rollback.

Logs und Monitoring: volle Festplatten vermeiden

Die von launchd umgeleiteten stdout.log/stderr.log werden standardmäßig nicht automatisch rotiert. Bei einem dauerhaft laufenden Dienst kommen so innerhalb weniger Monate mehrere GB zusammen. Mit dem systemeigenen newsyslog eine Regel hinzufügen:

/Users/deploy/apps/vapor-api/logs/stdout.log  deploy:staff  644  7  10240  *  N

Diese Regel besagt: Sobald das Log 10 MB überschreitet, wird rotiert, maximal 7 historische Dateien werden behalten. Die SSD-Kapazität eines Cloud-Mac-mini ist fest — ausufernde Logs gehen direkt zulasten des Platzes für Build-Artefakte und Snapshots. Es empfiehlt sich, wöchentlich mit du -sh ~/apps/vapor-api/logs einen Blick auf den Wachstumstrend zu werfen.

Stolperfallen

  • Verlorene Umgebungsvariablen: Ein von launchd gestarteter Prozess erbt keine per export in der .zshrc gesetzten Variablen. Datenbank-Connection-Strings und andere Konfigurationswerte gehören in das EnvironmentVariables-Dictionary der plist oder in eine separate .env-Datei, die im Code explizit geladen wird.
  • Portbelegung nicht erkennbar: Meldet launchctl load "Dienst existiert bereits", während der Port trotzdem nicht erreichbar ist, liegt meist ein verwaister plist-Eintrag von einem vorherigen unerwarteten Absturz vor. Zuerst launchctl remove, dann erneut load.
  • KeepAlive-Endlosschleife: Ein falscher Konfigurationspfad lässt den Prozess sofort abstürzen, KeepAlive startet ihn dann unbegrenzt neu, die CPU-Auslastung schießt schlagartig hoch. Zuerst mit launchctl unload stoppen, dann die Ursache suchen.
  • Kein Rollback-Punkt im Release-Skript: Das Verzeichnis current sollte als symbolischer Link auf das jeweilige releases/build-*-Verzeichnis zeigen. Ein Rollback besteht dann nur darin, den Link auf das vorherige Verzeichnis zurückzusetzen und neu zu laden — kein überschreibendes Deployment verwenden.

Häufig gestellte Fragen

Warum nicht einfach nohup oder eine Screen-Session für Vapor nutzen?

nohup trennt den Prozess nur vom Terminal ab, startet ihn aber nach einem Absturz oder Neustart nicht neu. launchd ist die systemweite Prozessverwaltung von macOS und kann per KeepAlive automatisch neu starten sowie den Dienst beim Boot aktivieren – wichtig für einen langfristig gemieteten Mac mini.

Braucht man für Zero-Downtime wirklich zwei Ports?

Ein Neustart auf demselben Port hinterlässt immer eine Lücke, in der Verbindungen fehlschlagen, bis der neue Prozess bereit ist. Mit einem zweiten Port, einem Health-Check und anschließendem Nginx-Upstream-Wechsel läuft der alte Prozess bestehende Verbindungen aus, während neue Anfragen an die gesunde Instanz gehen.

Sollte die Datenbank auf demselben Mac mini laufen?

Für kleine Workloads reicht eine lokale PostgreSQL- oder SQLite-Instanz mit täglichen Snapshots. Bei größeren Datenmengen oder mehreren Instanzen sollte eine separate verwaltete Datenbank angebunden werden, damit der Mac mini nur die Anwendungsschicht übernimmt.

Auf einem dedizierten Mac mini testen

Tagesweise mietbar, mit Root-Zugriff und Bereitstellung in wenigen Minuten – ideal, um erst zu testen und dann über eine längere Laufzeit zu entscheiden.

Jetzt bestellen