knuspermagier.de
Der privateste Blog von Philipp.

Homelab-Autodeployment

In den letzten Wochen habe ich – ja, gemeinsam mit Claude und Codex – an meinem Deployment im Homelab (und darüber hinaus) gearbeitet. Ursprünglich hatte ich mich ja mit Komodo für eine etablierte Software entschieden, aber nach ein bisschen Rumspielen damit erschien mir das alles zu fett und unübersichtlich. Ich warf es also wieder weg und baute meine eigenen übersichtlichen Skripte.

Jetzt, nachdem ich alle Zahnräder beisammen habe, erinnere ich mich wieder an das völlig überladene und komplizierte UI von Komodo und kann immerhin die ein oder andere Funktion gut nachvollziehen. Alles daran war vielleicht doch nicht übertrieben, ich musste es nur erst einmal nachbauen, um es zu verstehen.


Meine Zahnräder sehen also zusammengefasst so aus:

1) Infra-Repo

In meinem selbst gehosteten Forgejo habe ich für jeden Server ein Repository, in dem beschrieben ist, was dort alles als Docker-Container läuft. Für zu Hause ist z. B. infra-home. Das besteht aus zwei Teilen:

  • server/ für die Konfiguration der globalen Sachen – Caddy, der Backup-Service etc.
  • stacks/ für die einzelnen Stacks. Ein Stack ist dabei jeweils eine compose.yaml, in der die Container beschrieben sind.
file://lefpchzfehec6vsu

Secrets manage ich aktuell noch nicht über git, die werden manuell auf dem Server in .env-Dateien abgelegt, die von der compose.yaml referenziert werden.

2) Woodpecker CI

Ich nutze weiterhin Woodpecker. Wenn ein Infra-Repo gepusht wird, wird hier geschaut, ob alles passt. Es werden ein paar Regeln gecheckt, zum Beispiel, dass ich in den compose.yaml-Dateien keine Secrets direkt reingeschrieben habe und dass ich bei nicht selbstgebauten Containern keine :latest-Tags benutze. (Nervig, aber wer schon mal nach einem random docker compose pull erst mal eine halbe Stunde ein postgres-Update fixen musste, wird es verstehen.)

Diese Überprüfung ist etwas, das ich niemals selbst gebaut hätte, wenn ich alles selbst gemacht hätte – denn wer hat Zeit für so etwas, wenn man sich privat etwas baut? Da die KI das aber einfach für sinnvoll fand, nahm ich das natürlich gerne mit.

3) nana

Als ich mich dagegen entschied, Komodo zu verwenden, ließ ich nana bauen. Das ist ein kleiner Daemon, der einfach nur auf einen Webhook reagiert, das Git-Repository neu auscheckt und guckt, in welchen Stacks sich was geändert hat, um diese neu zu starten.

file://tqacztpye6sawyas

Am Anfang hatte ich also in der .woodpecker.yaml der infra-Repos immer noch einen curl auf den entsprechenden nana-Server.

4) Eigene Services

Natürlich hoste ich ja nicht nur externen Kram, sondern zu einem großen Teil auch selbst entwickelte Sachen. Die sollen natürlich auch entsprechend möglichst leicht ihren Weg auf den Server finden.

Um das zu vereinfachen, habe ich darauf geachtet, dass sich jedes Projekt in der Woodpecker CI zu einem Docker-Container bauen lässt. Egal, ob Node.js-, Go- oder Laravel-Projekt, das Endergebnis ist das gleiche.

Nun stand ich vor einem kleinen Problem: Wie sage ich dem Server, dass er bitte neu deployen soll, sobald das Paket fertig ist, ohne dass ich jedem Projekt mitteilen muss, auf welchem Server es gehostet ist? Also, es braucht ja nur einen curl an Nana, aber an welches?

Um der Pipeline das mitzuteilen, hätte ich es hart in die .woodpecker.yml schreiben müssen oder es über eine Variable in Woodpecker pflegen. Das ist ja aber wirklich Käse.

Ich brauchte also doch noch ein übergreifendes Tool.

5) Waltraut

Glücklicherweise hatte ich ja bereits vor Monaten mal eine Laravel-App gebaut, in der ich alle meine laufenden Dienste (manuell) eingetragen habe, die sich zum Beispiel um Health-Checks kümmert und auch Error-Logs über die eingebaute Sentry-Schnittstelle einsammelt.

file://ltwfag4swzeakwdr

Statt die Services manuell anzulegen, kann man hier nun also eine URL eines infra-Repos angeben und alles wird automatisch importiert. Zusätzlich kennt Waltraut die URL der entsprechenden nana-Instanz und kann dort klingeln, wenn etwas los ist.

Aber wie bekommt Waltraut nun mit, dass sich etwas verändert? Zum einen bestand die KI darauf, dass es sinnvoll ist, Forgejo einfach alle fünf Minuten zu pollen, falls ein Webhook mal fehlschlägt. Ja, okay.

Da ich auf so etwas allerdings nicht warten kann, ehrlich gesagt, und mir selbst eine Minute Wartezeit bis zum nächsten Poll zu viel wäre, musste natürlich auch der Webhook her.

Ich baute also erst mal in jede .woodpecker.yml einen entsprechenden curl ein, der mitteilte: „Hier, das Image von Projekt foobar ist neu!“ Waltraut schaute dann, auf welchem Server das Image benutzt wird, und sagte der nana-Instanz Bescheid.

Das funktionierte gut, war aber nervig. Ich hasse jede Zeile zu viel in der .woodpecker.yml, vor allem Zeilen, die ich in jedes Projekt reinkopieren muss. Das muss besser gehen.

Irgendwann kam mir die Idee, die globalen Hooks von Forgejo zu benutzen.

Man kann einfach immer dann einen Webhook triggern, wenn ein neues Docker-Image gepusht, also – um in Forgejo-Speak zu bleiben – ein neues Package erstellt wurde. Ich konnte also den ganzen Kram aus der CI-Konfiguration wieder entfernen.

Flow

Wenn ich mir nun also überlege, dass ich jetzt Tool XYZ in meinem Homelab installieren will, könnte ich so vorgehen:

  • Ich sage der KI, dass sie im infra-home-Repo einen Stack für foobar anlegen und mir eine beispielhafte .env-Datei erstellen soll. (Oder mache diesen Schritt, ganz verrückt, manuell)
  • Ich fülle die .env-Datei aus und speichere sie auf dem Server
  • Ich merge den Pull Request für das infra-Repo
  • Woodpecker checkt es und sagt Waltraut Bescheid, dass sich was geändert hat
  • Waltraut sagt der entsprechenden nana-Instanz Bescheid, die checkt das Repository aus und startet den foobar-Stack
  • Zwei Minuten später hat Caddy es auch geschafft, ein neues SSL-Zertifikat zu besorgen, und ich kann foobar.example.com aufrufen.

Genauso funktioniert das im Grunde auch für selbst entwickelte Sachen:

  • Ich sage der KI, dass sie mir bitte Tool XYZ entwickeln und in ein vorgegebenes Git-Repo pushen soll. Oder ich mache es halt selber.
  • Woodpecker baut das Image und packt es in die Forgejo-Registry
  • Der Rest ist exakt so wie oben.
file://1t6k8emtw98hqbgj
Grün: Dinge, die ich noch selber machen muss.

Hätte ich mir jetzt lieber Komodo genauer angucken sollen, statt alles selber zu bauen? Vielleicht wäre das schneller gegangen. So hat es aber mehr Spaß gemacht und mindestens 50 % der Sachen hätte ich eh machen müssen, da man ja auch erst mal alle Projekte dazu kriegen muss, sich korrekt zu verhalten.

Ist meine Lösung production-ready, komplett abgesichert und vor Ausfällen geschützt? Sicherlich nicht, aber da ich 90 % davon sowieso nur zu Hause betreibe und die nana-Instanzen der öffentlichen Server auch nur per Tailscale erreichbar sind, ist das verschmerzbar.

Abgesehen davon kann man über letztere eh nur sagen: „Hier, trigger mal einen Deploy.“ Außer einem DDoS wäre da jetzt auch nicht so viel Angriffspotenzial.

Das Wichtigste an so einem Setup ist ja, dass es übersichtlich und gut dokumentiert ist. Ist ja nicht so, als hätte ich in meinem Webhosting-Leben nicht schon tausende Varianten von automatischen Pipelines in diversen Ausbaustufen gehabt. Meistens ist es so, dass man es entweder nicht fertig baut oder sich nicht merken kann, wie es funktioniert, und vergisst, es zu dokumentieren.

Hier habe ich nun immerhin einen guten Überblick; die Sache ist überschaubar und dank des unermüdlichen Willens der KI sogar dokumentiert. Vor allem das Letzte ist eine Sache, die ich bei privaten Sachen früher natürlich niemals gemacht hätte. Dokumentieren, hahaha.

Kommentare, Feedback und andere Anmerkungen?
Schreib mir eine E-Mail 🤓