Wie ich meine Server betreibe (2022)
(blog.wesleyac.com)- Persönliche Services betreibe ich direkt auf einer DigitalOcean-VM für 5 $/Monat und Debian 10; für kleine Services ist es völlig einfach und sinnvoll, Server-Software direkt auf einer VM laufen zu lassen
- Server-Apps werden als statische Rust-Binärdateien gebaut, wobei HTML, CSS, Konfiguration und sogar Geheimnisse eingebettet werden, sodass das Deployment-Artefakt aus einer einzigen Datei besteht
- Das Prozessmanagement übernimmt systemd, HTTPS wird über einen nginx-Reverse-Proxy und Let’s Encrypt/certbot abgewickelt, sodass die App TLS nicht selbst behandeln muss
- Für Services mit Datenbedarf verwende ich eine einzelne SQLite-Datei und kombiniere tägliche Tarsnap-Backups mit Streaming-Backups über Litestream nach DigitalOcean Spaces
- Wenn mehrere Services auf einer VM laufen, isoliere ich sie über separate Unix-Benutzer pro Service; bei höherem Sicherheitsbedarf nutze ich separate VMs oder systemd-nspawn bzw. firejail
Grundaufbau von Servern und Deployments
- Mehrere Services wie thoughts.page, hanabi.site, cgmserver und phonebridge laufen ungefähr in dieser Konfiguration
- Die Apps laufen auf einer DigitalOcean-VM im Tarif für 5 $/Monat mit Debian 10 als Betriebssystem
- Einige Services teilen sich dieselbe VM, andere sind auf getrennte VMs verteilt
- Die Server-Software ist in Rust geschrieben
- statisch gelinkt
- HTML, CSS, Konfiguration und Geheimnisse werden in die Binärdatei kompiliert
- Verwendet werden rust-musl-builder und rust-embed
- Dadurch wird ein Deployment im Wesentlichen darauf reduziert, eine einzige Datei auf den Server zu kopieren
- Ein ähnlicher Ansatz ist auch mit Go, C++ usw. möglich
- Wenn eine Sprache Single-Binary-Deployments nicht gut unterstützt, kann alternativ ein Docker-Container als Build-Artefakt dienen
Ausführung, Proxy und Datensicherung
- Das Starten der Services übernimmt systemd
- Beim Serverstart werden die Binärdateien automatisch mitgestartet
- Die meisten systemd-Unit-Dateien sind einfache Dateien mit 9 Zeilen
- systemd selbst ist zwar komplex, aber für den Zweck, Server beim Booten zu starten, bekommt man von dieser Komplexität meist wenig mit
- Deployments erfolgen über ein einfaches Deploy-Skript
- Es kopiert die Binärdatei auf den Server und startet den Server neu
- Rollbacks sind möglich, und selbst wenn während des Deployments die Verbindung abbricht, läuft immer eine gültige Version
- Für Programme, die eine Datenbank brauchen, verwende ich SQLite
- Der komplette App-Zustand liegt in einer einzigen Datei
- Täglich wird mit dem SQLite-Befehl
.backupein Backup erstellt und in Tarsnap gespeichert - Das Backup-Skript läuft per cron
- Mit Litestream wird eine Datenbankkopie im Sekundentakt in den DigitalOcean-Spaces-Storage gestreamt; Snapshots werden alle 6 Stunden erzeugt
- Alle Server laufen hinter einem nginx-Reverse-Proxy
- nginx übernimmt die TLS-Terminierung, sodass sich die App nicht um HTTPS kümmern muss
- HTTPS-Zertifikate werden mit Let’s Encrypt und certbot bezogen und automatisch erneuert
- Ein Beispiel für die nginx-Konfiguration von hanabi.site gibt es in einem separaten Gist
- Statische Dateien können von nginx ausgeliefert und per
scpoderrsyncauf den Server kopiert werden
Wartung und Service-Isolierung
- Diese Konfiguration zielt auf einen einfachen und robusten Betriebsweg ab
- Mit Ausnahme der App selbst besteht der Auslieferungspfad aus bewährten Komponenten, die seit Jahrzehnten eingesetzt werden
- Solange man die DigitalOcean-Rechnung bezahlt, gibt es für den Betrieb der Sites im Wesentlichen kaum Wartungsaufwand
- Die einzigen von Monitoring erkannten Probleme waren vorübergehende Netzwerkprobleme bei DigitalOcean
- Betriebssystem- und Sicherheitsupdates sind gelegentlich nötig
- Debian-Releases erhalten 5 Jahre Support, daher ist in etwa zweieinhalb Jahren ein Upgrade auf Debian 11 nötig
- Wenn noch einmal ein Vorfall wie Heartbleed passiert, muss gepatcht werden
- Solche Vorfälle sind selten
- Für jeden Service eine eigene VM für 5 $/Monat zu nutzen, kann teuer werden, aber mehrere Services können auf derselben VM laufen
- Die Isolation erfolgt über separate Unix-Benutzerkonten pro Service
- Diese Form der Isolation gibt es seit den Anfängen von Unix und sie wirkt entsprechend robust
- Wenn stärkere Isolation nötig ist, können systemd-nspawn oder firejail verwendet werden
- Wenn es wirklich sicherheitskritisch ist, zahle ich weitere 5 $/Monat und lasse es auf einer separaten VM laufen
- Die Einrichtung eines neuen Projekts ist kurz
- neuen Benutzer anlegen
- nginx-Virtual-Host hinzufügen und mit certbot ein HTTPS-Zertifikat ausstellen
- systemd-Unit hinzufügen
- das Deploy-Skript ins Repository committen und ausführen
- Am Anfang gibt es viel zu lernen, aber diese Infrastruktur verändert sich deutlich langsamer als Cloud-Infrastruktur
- Das Format der nginx-Konfiguration ist in den letzten 10 Jahren im Wesentlichen fast gleich geblieben
- Die letzte große Veränderung in der Debian-Systemverwaltung war der Wechsel zu systemd vor fast 10 Jahren
- Man ist weniger Situationen ausgesetzt, in denen ein Cloud-Anbieter Services einstellt oder Verhalten stillschweigend ändert
- Die einzige Abhängigkeit ist der VPS-Anbieter; Server sind jedoch ein austauschbares Standardprodukt, sodass man zu einem anderen Anbieter wechseln kann
1 Kommentare
Hacker-News-Kommentare
Wenn man jeden Dienst zur Isolation unter einem eigenen Unix-Benutzer laufen lässt, kann systemds DynamicUser-Funktion Zeit sparen
Sie weist eine UID zu und legt Log-/Statusverzeichnisse mit den richtigen Berechtigungen an
https://0pointer.net/blog/dynamic-users-with-systemd.html
Man legt eine kleine Textdatei mit Informationen wie Benutzername und Home-Verzeichnis in
/etc/sysusers.d/ab und führt dann densysusers-Befehl oder -Dienst ausHTTP-getriggerte Cloud Functions gefallen mir derzeit am besten
Wenn man die anbieterspezifischen Fallstricke sorgfältig umgeht, reduzieren sie die Komplexität stark und wirken unter den Cloud-nativen Abstraktionen als einzige wie eine „Endform“
Ich habe allein in einer App über 2000 Deployments gemacht und musste nie das Supportteam kontaktieren oder obskure Konsolenbefehle eingeben, weil die Laufzeitumgebung kaputtgegangen wäre
Auch die Performance auf Basis von Azures isoliertem App-Service-Plan ist hervorragend, und meine ideologische Abneigung gegen Abrechnung pro Request ist inzwischen verschwunden
Man könnte es billiger machen, wenn man sogar die Immobilie besitzt, in der die eigenen Server stehen, aber um Eigenschaften wie Compliance und Auditierbarkeit, die eine einfache HTTP Function bietet, selbst zu implementieren, müsste man faktisch ein riesiges Unternehmen aufbauen und jede Menge Leute einstellen
Auch das Argument des Vendor-Lock-ins überzeugt mich nicht mehr wirklich. Eine Schnittstelle, die einen HTTP-Request entgegennimmt und eine HTTP-Response zurückgibt, ist natürlich, und die anbieterspezifischen Unterschiede beschränken sich auf zusätzlichen Kontext wie die Signatur der Trigger-Methode oder OIDC-/SAML-Claims. Man müsste sich also eher anstrengen, eine Struktur zu bauen, die sich nicht innerhalb einer Woche auf einen anderen Anbieter refaktorisieren ließe
Bei einem persönlichen Blog verstehe ich, dass es mehr Spaß macht, eine Hetzner-VM zu kaufen und handwerklich feinzujustieren. In einer völlig unregulierten Branche ließe sich auch stärker dafür argumentieren, Margen und Komplexität genauer abzuwägen, statt Server vollständig auszulagern
Ich weiß nicht, wie sich die Leute vorstellen, dass das vor der Cloud-Migration gemacht wurde
Ich betreue derzeit in einem großen Industrieunternehmen mehrere Teams und skaliere und deploye Innovations-Apps auf Azure, und die Cloud-Kosten sind im Verhältnis zur Nutzerzahl absurd hoch
Früher haben wir Apps mit der zehnfachen Nutzerzahl zu einem Hundertstel der Kosten und mit geringerer Komplexität betrieben
Da diese Kosten an eine andere Abteilung weiterverrechnet werden, die diese fragwürdige Entscheidung getroffen hat, ist es mir persönlich egal, aber die blinde Gläubigkeit gegenüber der Cloud kann ich schwer nachvollziehen
Für uns ist eigene Hardware zu betreiben die beste Lösung, und da man nicht einmal im Büro Fotos machen darf, liegt die Wahrscheinlichkeit, einem Cloud-Anbieter irgendetwas anzuvertrauen, bei 0 %
Aber damit eine App nützlich wird, braucht sie auch andere Komponenten wie eine Datenbank, und entstehen nicht genau dort zusätzliche Kosten?
Ich bin unsicher, ob es um Request-Handler eines HTTP-Servers geht, um Function-as-a-Service-Cloud-Computing oder einfach um altes RPC
Wenn irgendein Scriptkiddie anfängt, zehnmal pro Minute Requests zu schicken, wie lange dauert es dann, bis die Function-Kosten teurer sind als ein VPS?
Für persönliche Websites und Projektseiten verwende ich eine ähnliche Konfiguration
Ich nutze eine Linode-VM für 5 Dollar im Monat, setze Debian GNU/Linux ein, und die Software ist in Common Lisp geschrieben
Für persönliche Websites oder Blogs generiert ein Common-Lisp-Programm statische Seiten, und Online-Dienste oder Web-Apps baue ich als Common-Lisp-Programme, die mit Hunchentoot HTTP-Requests verarbeiten und beantworten
Damit Sites/Dienste beim Start oder Neustart der VM automatisch hochkommen, verwende ich systemd-Unit-Dateien, die meist etwa 10 bis 15 Zeilen lang sind
Die Ersteinrichtung der VM ist als Shell-Skript codiert: https://github.com/susam/dotfiles/blob/main/linode.sh
Projekt- und dienstspezifische Konfigurationen verwalte ich jeweils per Makefile: https://github.com/susam/susam.net/blob/main/Makefile, https://github.com/susam/mathb/blob/main/Makefile
Container verwende ich nicht. Diese Sites laufen schon seit Jahren, seit einer Zeit vor dem Container-Hype, und bisher haben Initialisierungsskripte und Makefiles ausgereicht
Nginx nutze ich ebenfalls. Es liefert statische Dateien aus, fungiert bei Backend-Diensten als Reverse Proxy und bietet neben TLS-Terminierung weitere Vorteile wie Request-Rate-Limiting oder Allowlist-Konfigurationen für HTTP-Header zum Schutz des Backends
In meinem kleinen persönlichen Playbook stehen ein paar Befehle wie
curl LINK -o linode.sh && sh linode.shundgit clone LINK && cd PROJECT && sudo make setup httpsDie
make-Targets erledigen die für den Betrieb der Website nötigen Aufgaben, etwa die Installation von Tools wie Nginx, certbot und sbcl, die Nginx-Konfiguration und die Zertifikatseinrichtung; sobald der Befehl beendet ist, ist die Website direkt öffentlich erreichbarDem Makefile nach scheint es SBCL zu sein; ich frage mich, ob der Speicherverbrauch je ein Problem war
Ich nutze einen VPS mit 512 MB RAM, und eine SBCL-Instanz verbraucht ungefähr 100 MB, sodass nur ein paar Dienste gleichzeitig laufen können
Ich habe auch darüber nachgedacht, den Dienst mit dem geringsten Traffic auf CLISP umzuziehen, aber dort fehlt ein von mir verwendetes Feature, nämlich package-lokale Aliasse
Nachdem ich die Konfiguration über Jahre hinweg immer weiter verfeinert habe, bin ich inzwischen dabei gelandet, alles als Docker-Container laufen zu lassen
Als Orchestrator verwende ich docker-compose statt systemd, als Proxy Caddy statt nginx
Wie im ursprünglichen Artikel schreibe ich für jedes Projekt, das laufen soll, ein Deployment-Skript, daher ist es insgesamt ziemlich ähnlich
Einer der vielen Vorteile von Docker ist, dass man damit auch Third-Party-Software mit derselben Konfiguration ausführen kann
Ich nutze dieses Setup seit einigen Jahren, und es ist sehr gut; wie im Originalartikel ist es robust, bietet aber bei Bedarf auch die Flexibilität, Dinge nach Wunsch anzupassen
Der einzige aktuelle Schmerzpunkt sind Rolling Deployments. Da die Software größer wird, werden ein paar Sekunden Downtime bei jedem Deployment zum Problem, und ich habe noch keine einfache Lösung, aber docker swarm könnte der richtige Weg sein
Um Downtime zu reduzieren, kann man
health_uri /healthundlb_try_duration 30sausprobierenWenn man es zum Beispiel wie
reverse_proxy api:8089 { health_uri /health lb_try_duration 30s }konfiguriert, puffert Caddy während des Deployments der neuen Version die Requests und gibt dem neuen Service 30 Sekunden Zeit, hochzukommenIdealerweise sollte Caddy die neue Version erst verwenden, nachdem sie healthy ist, und dann den alten Container beenden
Ich habe mir https://github.com/Wowu/docker-rollout und https://github.com/lucaslorentz/caddy-docker-proxy angesehen, aber noch keine Priorität dafür gefunden
Perfekt ist das nicht, aber statt dass der Browser einen Verbindungsfehler bekommt, lädt er einfach ein paar Sekunden lang
https://mrsk.dev/ nutzt dieselbe Technik
In Produktion sind wir mit compose gestartet und haben das später zu einer Continuous-Deployment-Pipeline ausgebaut, die den Stack automatisch aktualisiert
Als das Unternehmen und die Nutzerbasis wuchsen, traten Probleme mit Downtime bei Neustarts oder Deployments auf, und jedes Mal, wenn wir zusätzliche Apps betreiben wollten, mussten wir eine neue Deployment-Umgebung vorbereiten
Ich fürchtete den Tag, an dem wir Kubernetes einsetzen müssten; ich hatte dessen Komplexität aus nächster Nähe gesehen und wollte nicht den Großteil des Tages damit verbringen, einen Cluster zu besänftigen
Deshalb sind wir zu Swarm mode gegangen, und die Reise ist mal Jekyll, mal Hyde
Es gibt Bugs, die niemand beheben will, Teile der Docker-Spezifikation, die nicht implementiert sind, ohne dass einem das gesagt wird, Implementierungsentscheidungen, bei denen man sich die Haare rauft, und das Gefühl, dass die Mitarbeiter von Docker Inc nicht miteinander reden oder Dinge nicht bis zum Ende durchziehen
Trotzdem gibt es auch viele schöne Seiten. compose-Stacks funktionieren unverändert und können genau dort wachsen, wo es nötig ist; wenn es richtig eingerichtet ist, funktionieren Zero-Downtime-Deployments, Upgrades, Load Balancing und Rollbacks gut
Raft ist wie anderswo zuverlässig für den Cluster-Erhalt, und mit etwas Aufwand kann man mit einem Bruchteil des K8s-Wartungsbudgets eine flexible, sichere und automatisch verteilte Self-Service-Plattform bauen
Allerdings muss man bereit sein, die Deployment-Skripte ordentlich zu bauen. Ich habe ziemlich viel Zeit damit verbracht, ein Python-Tool zu erstellen, das gültige Docker-Spec-compose-Dateien in die Swarm-Spezifikation umwandelt, Secrets erneuert und aufräumt und Umgebungsvariablen erweitert
Je nach VPS-Anbieter muss man außerdem die Netzwerk-MTU unbedingt korrekt setzen. Das dürfte meine Lebenserwartung deutlich verkürzt haben
Zumindest nicht, bevor durch viele Kunden oder Nutzer tatsächlich Skalierungsprobleme entstehen
docker saveunddocker importper SSH hineinzuschiebenIch frage mich, ob du eine eigene Registry betreibst
Auf einem physischen Server packe ich die PostgreSQL-Datenbank und die App in einen Podman-Pod, lasse alles auf localhost-Ports oberhalb von 5000 laufen, und Caddy agiert auf 443 als Reverse Proxy
Mit einem systemd Timer dumpe ich täglich um 16:55 Uhr alle Datenbanken in ein Verzeichnis
Danach sichert DejaDup [1] jeden Tag um 17:00 Uhr automatisch
$HOMEauf eine externe HDD; Cache-Dateien werden ausgeschlossen, die Datenbank-Dumps aber eingeschlossenAls OS läuft Debian mit GNOME Core [2], und die firewalld-Regeln erlauben nur 80, 443 und einen benutzerdefinierten SSH-Port
SSH ist keybasiert, Passwortauthentifizierung ist deaktiviert
Das ist die langweiligste Methode, aber sie funktioniert einfach gut
1 - https://flathub.org/apps/org.gnome.DejaDup
2 - https://packages.debian.org/bookworm/gnome-core
Heutzutage nutze ich Dokku auf einem einzelnen Server
Es ist leicht zu verwalten, Entwickler können wie bei Heroku einfach per
git pushdeployen, und dank vieler Plugins dauert das Hinzufügen einer Datenbank oder das Einrichten von HTTPS höchstens 10 Sekundengit pushdeployen zu könnenApp hinzufügen, Datenbank hinzufügen, Umgebungsvariablen verwalten und Domains verwalten ist alles sehr intuitiv
Ich mag einfache Setups, daher sieht diese Art zu arbeiten angenehm aus
Für 99 % der Dienste dürfte das wahrscheinlich ausreichen
Für Server-Konfiguration und Deployment-Skripte würde ich wohl Ansible verwenden
Damit wäre der Server dokumentiert, und auch die Deployment-Skripte könnten einfacher werden
Allerdings würde ich nicht darauf verzichten, mich zum Debuggen oder Überprüfen direkt auf dem Server einzuloggen
Meine Skripte haben sich in den letzten 7 Jahren kaum verändert, während Ansible langsam ist und dazu neigt, dass man mehrere Dateien ständig pflegen muss
Was Debugging angeht, könnte man die App-Logs auch per nginx über einen passwortgeschützten Endpunkt zugänglich machen
Man vergisst offenbar manchmal, dass CI/CD und effektives Server-Management schon vor der Cloud gängige Praxis waren
Zur Methode, Serversoftware in Rust zu schreiben, statisch zu linken und HTML, CSS, Konfiguration, Secrets usw. alles in das Binary hineinzukompilieren: Ich mache das seit Kurzem in Go so und finde es großartig, weil das Schreiben und Ausliefern von Software, die auf statische Dateien angewiesen ist, dadurch wirklich einfach wird
Für Secrets sind Umgebungsvariablen oder Konfiguration eher der Standard; zwar kommt dadurch ein Schritt vor dem Start hinzu, aber man vermeidet, ein Binary weiterzugeben und später zu vergessen, dass darin Secrets enthalten waren
Ein Binary zu entpacken und nach Strings zu suchen, ist ziemlich einfach
Statische Frontend-Assets und Default-Konfiguration mit hineinzupacken, ist dagegen ein großer Vorteil
Cross-Compiling mit Rust, insbesondere von Mac nach Linux, ist vergleichsweise schmerzhaft, während es mit Go trivial und in das
go-Tool eingebaut istEs wird wirklich leicht, beim Abschließen und Deployen eines Nebenprojekts Reibung zu vermeiden
Allerdings habe ich eine Frage. Im Artikel heißt es: „Let’s-Encrypt-Zertifikate werden mit certbot bezogen, und die automatische Erneuerung wird ebenfalls erledigt.“ Ich habe jedoch ein regionenübergreifendes Setup mit zwei Servern und Geo-DNS, sodass certbot nur auf dem US-Server läuft und ich das Zertifikat manuell auf den europäischen Server kopieren muss
Gibt es eine Möglichkeit, das zu lösen?
Die Beschreibung des ClickHouse Playground ist hier: https://ghe.clickhouse.tech/
Dieselbe statisch kompilierte Service-Anwendung, die die Backend-API bereitstellt, liefert auch die Webanwendung aus
Wenn ich in CI
npm buildausführe und das Ergebnis einbette, wird es sehr einfach, lokale Tests oder eine Demo-Instanz zu starten