4 Punkte von GN⁺ 2024-03-17 | 1 Kommentare | Auf WhatsApp teilen
  • Wenn Docker-Images bereits als Deployment-Einheit verwendet werden, ist Nix ein Einstiegspunkt, um deterministische Builds und festgeschriebene Abhängigkeiten zu erproben, ohne den Workflow stark zu verändern
  • Ein gewöhnliches docker build verlässt sich leicht auf den Zustand des öffentlichen Internets, etwa auf Ubuntu-Repositories oder externe Downloads, sodass es schwierig wird, dasselbe Image später erneut zu reproduzieren
  • Nix kennt die benötigten Abhängigkeiten auf Paketebene, sodass mit dockerTools.buildLayeredImage eine Konfiguration möglich ist, bei der nur geänderte Layer erneut hochgeladen werden
  • Das Go-basierte Beispiel douglas-adams-quotes zeigt, dass sich mit pkgs.buildGoModule ein Binary bauen und mit nix build.#docker, docker load <./result wie ein gewöhnliches Docker-Image laden und deployen lässt
  • Wenn mehrere Services in einem Monorepo Layer und den Nix-Cache gemeinsam nutzen, lassen sich bei der Reproduktion von Images aus älteren Commits und bei wiederholten Deployments Build-Zeit und Kosten senken

Warum Nix an Docker-Image-Builds anschließen?

  • Nix vereint drei Eigenschaften: Paketmanager, Sprache und Betriebssystem
    • Mit der Nix-Sprache werden Anweisungen zum Paket-Build geschrieben
    • Der Nix-Paketmanager kann auf Basis dieser Anweisungen Software, Tools, NixOS-Images, Container-Images und mehr erzeugen
  • Nix in bestehende CI/CD-Pipelines einzuführen, ist nicht einfach
    • Nix unterscheidet sich von den Arbeitsweisen, an die viele Entwickler gewöhnt sind
    • Außer in Umgebungen wie neuen Startups oder Homelabs, in denen alles neu aufgesetzt werden kann, ist die Einstiegshürde hoch
  • Organisationen, die bereits Docker verwenden, können Nix zunächst beim Bau von Container-Images einsetzen und so experimentieren, ohne den bestehenden Deployment-Ablauf stark zu stören

Wo Docker-Builds ins Wanken geraten

  • Docker und Containerisierung sind breit etabliert, und Docker-Images werden im Internet faktisch wie ein universelles Paketformat verwendet
    • Plattformen wie Fly.io, Railway und Render verwenden Docker-Images statt beliebiger VM-Images oder Linux-Programme als Tarball
  • Docker-Builds sind nicht immer deterministisch
    • Die meisten Dockerfiles, die man im Internet sieht, bauen zwar korrekt, aber schon ein kleiner Anteil von Fehlschlägen kann zu Betriebsproblemen führen
  • Eine der größten Schwachstellen ist, dass Docker-Builds auf das öffentliche Internet zugreifen
    • Das ist nötig, um Pakete aus Ubuntu-Repositories herunterzuladen
    • Wenn später dasselbe Image erneut erzeugt werden muss, ist es schwierig, den exakten Zustand des damaligen Ubuntu-Repositories wiederherzustellen
    • Ubuntu 18.04 nähert sich in diesem Jahr dem Support-Ende, und von welchen Dingen diese Version abhängt, merkt man womöglich erst nach einem Ausfall
  • Wenn Pakete auf einfache Weise zu einem Docker-Image hinzugefügt werden, kann verschwendeter Speicherplatz entstehen
    • Wird früh im Build apt-get upgrade ausgeführt, können Dateien im Container-Image ersetzt werden
    • Die vorherigen Dateien bleiben möglicherweise im Layer erhalten und sammeln sich als Schattenkopien an

Wie Nix mit Docker-Layern umgeht

  • Nix kennt die benötigten Abhängigkeiten im Voraus und kann sie auf möglichst wenige Docker-Layer aufteilen
    • Wenn nur eine Zeile Code geändert wird, müssen apt oder npm die Abhängigkeiten nicht erneut installieren
    • So lässt sich bei einem Image-Update tatsächlich nur das minimal Geänderte berücksichtigen
  • dockerTools ist eine Werkzeugsammlung, um Nix-Pakete und ihre Abhängigkeiten in Docker-Images zu packen
  • Docker-Images aus Nix lassen sich grob in zwei Arten einteilen
    • Nicht-Layer-Images: Programm, Abhängigkeiten und zusätzliche Elemente wie TLS-Root-Zertifikate werden in einem Ordner zusammengefasst und als Single-Layer-Image bereitgestellt
    • Layer-Images: Jede Abhängigkeit wird in einem separaten Image-Layer abgelegt, sodass nur tatsächlich geänderte Teile hochgeladen werden müssen
  • Docker selbst hat ebenfalls einen inhaltsbasierten Speicher, aber mit docker build allein lässt sich das nur schwer vollständig ausnutzen
    • Nix-Layer-Images verwenden paketweise Layer, sodass Abhängigkeiten wie glibc nur einmal hochgeladen werden müssen
    • Wenn an einer solchen Bibliothek Änderungen nötig sind, muss dieser Layer allerdings erneut hochgeladen werden

Beispiel eines Go-Service: Douglas Adams Quotes

bin = pkgs.buildGoModule {
  pname = "douglas-adams-quotes";
  inherit version;
  src = ./.;
  vendorHash = null;
};
  • Diese Definition verwendet das Go-Modul-Template, um Go-Compiler, C-Compiler für CGo und den Download externer Abhängigkeiten einzurichten
    • pname ist der Paketname
    • version wird automatisch aus dem Git-Commit des Service erzeugt
    • src = ./.; verwendet den Quellcode aus dem aktuellen Arbeitsverzeichnis
    • Wenn es keine Abhängigkeiten außerhalb der Standardbibliothek gibt, kann vendorHash = null gesetzt werden
    • Gibt es externe Abhängigkeiten, muss entweder der Hash aller Abhängigkeiten angegeben oder gomod2nix verwendet werden
  • Das Paket wird mit folgendem Befehl gebaut
nix build .#bin
  • Ein Docker-Layer-Image kann mit dockerTools.buildLayeredImage erzeugt werden
docker = pkgs.dockerTools.buildLayeredImage {
  name = "registry.fly.io/douglas-adams-quotes";
  tag = "latest";
  config.Cmd = "${bin}/bin/douglas-adams-quotes";
};
  • Wenn das gebaute Server-Binary in config.Cmd angegeben wird, werden die benötigten Elemente mitkopiert
    • glibc und alles, was für die Ausführung nötig ist, wird ebenfalls einbezogen
  • Zusätzliche Pakete wie CA-Root-Zertifikate können in contents aufgenommen werden
docker = pkgs.dockerTools.buildLayeredImage {
  name = "registry.fly.io/douglas-adams-quotes";
  tag = "latest";
  contents = with pkgs; [ cacert ];
  config.Cmd = "${bin}/bin/douglas-adams-quotes";
};
  • Das Image wird mit den folgenden Befehlen gebaut und in den Docker-Daemon geladen
nix build .#docker
docker load < ./result
  • Öffnet man es mit dive, sieht man, dass jeder Layer ein anderes Paket aus nixpkgs hinzufügt und die Einträge am Ende per symbolischem Link in die Image-Root eingebunden werden

Der Effekt gemeinsam genutzter Layer im Monorepo

  • Wenn sich mehrere Services im selben Repository befinden, können mit Nix gebaute Docker-Images Layer gemeinsam nutzen
    • Das geschieht ohne separate Konfiguration
    • Mit Docker allein würde man dafür meist mehrere gemeinsame Basis-Images bauen, die dann Werkzeuge und Inhalte enthalten können, die manche Services gar nicht verwenden
  • Als Beispiel ist das Repository Xe/x ein Monorepo mit Nebenprojekten, Experimenten und Tools aus zehn Jahren
    • Mehrere Projekte werden auf ungefähr drei Plattformen deployt
    • Um auf eine gemeinsame Deployment-Basis hinzuarbeiten, wurden sie nach und nach in Docker-Images überführt
  • Wenn ein Update für einen Service gepusht wird, werden zugleich auch einige Updates gepusht, die mit den meisten anderen Services geteilt werden
    • Dieser Ansatz spart über mehrere Projekte hinweg Zeit und Kosten

Was sich allein mit Docker-Cache nur schwer nachbilden lässt

  • Mit Docker-Caching lassen sich Images theoretisch ebenso effizient bauen wie mit Nix
  • Um mit Docker denselben Effekt zu erzielen, können die Build-Schritte jedoch schwer wartbar werden
    • Gemeinsame Bibliotheken müssten in separaten Layern installiert werden
    • Während des Builds müsste der Netzwerk-Stack wieder aktiviert werden, was die Reproduzierbarkeit schwächt
    • Suchpfade, Compiler-Flags und CGo-bezogene Einstellungen müssten erneut angepasst werden
  • dockerTools.buildLayeredImage von Nix übernimmt die Details, wie Pakete in den Container gelangen, und vereinfacht dadurch die Konfiguration

Reproduktion vergangener Zeitpunkte und Nix-Cache

  • Einer der großen Vorteile von Nix ist, dass sich Software aus einem früheren Zeitpunkt exakt erneut bauen lässt
    • Wenn später ein Bug in einer bestimmten Kundenumgebung reproduziert werden muss, kann dasselbe Docker-Image erneut erstellt werden
    • Der Paket-Build friert den vollständigen Zustand von Software und Abhängigkeiten zu einem bestimmten Zeitpunkt ein
  • XeDN ist ein Projekt, an dem seit einigen Jahren gearbeitet wird; eine Version von vor 14 Monaten lässt sich mit folgendem Befehl bauen
nix build github:Xe/x/567fdc2#xedn-docker
  • Dieser Befehl baut das Ziel xedn-docker aus einem bestimmten Commit des GitHub-Repo Xe/x
    • Lädt man es in den Docker-Daemon, entsteht ein Image mit denselben Bytes wie damals
    • Im Beispiel ist auch Go 1.19 enthalten
  • Eine vergleichbare Reproduktion nur mit einem gewöhnlichen Docker-Build würde unter Umständen viel Speicher kosten
  • Der Nix-Cache speichert die Ausgaben von Nix-Befehlen, damit sie später nicht erneut gebaut werden müssen
    • So muss auf den Notebooks der Entwickler nicht jedes Mal ein Paket wie nokogiri neu gebaut werden
    • Stattdessen können bereits in der Cloud gebaute Ergebnisse bezogen werden
  • Garnix wird für die CI aller Flakes-Projekte verwendet und meldet für jeden Commit den Build-Status
    • Auch die Konfiguration der Homelab-Maschinen wird mit Garnix gebaut; bei abendlichen Updates wird dann die neueste Konfiguration aus dem Garnix-Cache geholt statt lokal neu zu bauen

Fazit als Deployment-Artefakt

  • Nix kann als Docker-Image-Builder als besserer Image-Builder gelten als Docker selbst
  • Nix zwingt dazu, das Ergebnis zu beschreiben, statt die detaillierten Schritte zum Erreichen dieses Ergebnisses einzeln aufzulisten
  • In Umgebungen, die bereits Docker verwenden, ist es ein realistischer Einführungsweg, mit dem Build von Docker-Images zu beginnen
  • Mit Nix gebaute Docker-Images können Layer zwischen verschiedenen Teilen eines Monorepos gemeinsam nutzen
  • Dank Binary-Cache muss Code, der in der Vergangenheit schon gebaut wurde, nicht erneut gebaut werden
  • Das Endergebnis ist ein gewöhnliches Container-Image, das sich auf Plattformen wie AWS, Google Cloud und Fly.io deployen lässt

1 Kommentare

 
GN⁺ 2024-03-17
Meinungen auf Hacker News
  • Ich habe mehrmals versucht, Nix zu mögen, aber inzwischen habe ich das Gefühl, dass ich es aufgeben sollte.
    Ich habe zwei Systeme, auf denen Nix läuft, aber ich habe Angst, sie anzufassen; früher sind mir beide schon einmal kaputtgegangen, und ich musste sie von Grund auf neu installieren. Theoretisch ist Nix idempotent und deterministisch, aber in der Praxis muss man verstehen, in Bezug worauf es deterministisch ist. Wenn man nicht genau weiß, wie alle abhängigen Teile funktionieren, bekommt man seltsame Ergebnisse und bizarre, nicht hilfreiche Fehler – oder gar kein Feedback.
    Die Dokumentation ist zwar vollständig und technisch korrekt, aber furchtbar schwer zugänglich, und Tutorials bringen einen nur etwa 80 % des Weges. Wenn man darüber hinausgeht, muss man es selbst bauen; das erfordert jahrelanges Erfahrungswissen und Frustration, was ich mir nicht mehr zumuten möchte. Der Vorteil von Docker ist dagegen gerade das Chaos selbst: Wenn man sich halbwegs mit Shell und dem Paketmanager der Distribution auskennt, kann man fast alles bauen, und wenn Probleme auftreten, lassen sie sich dank jahrzehntealter Tools viel leichter diagnostizieren und beheben.
    Nix ist wie Emacs: Mit Geduld und tiefem, visionärem Wissen kann man alles damit machen, aber offenbar bleibt einem nur, sich vollständig hineinzustürzen oder es aus der Distanz zu beobachten.

    • Ich bin denselben Weg gegangen, und deterministische Builds finde ich gut, aber bei Dockerfiles sehe ich das größte Problem darin, dass sie für durchschnittliche Entwickler deterministisch wirken.
      Wenn man auf derselben Maschine zweimal hintereinander baut, kommt dasselbe Ergebnis heraus. Aber wenn im Lauf der Zeit Paketmanager und Base-Image-Tags aktualisiert werden, kann ein Rebuild einen Monat später ein völlig anderes Ergebnis liefern. Wenn ein Team etwa 40 Container verwaltet, wird das Reparieren von Containern zu einem großen Teil der Arbeit.
      Deshalb sollte Nix theoretisch perfekt sein, aber es ist so anders, dass Vendor-Tools unter Nix nicht laufen, und wenn Fehler auftreten, findet man im Web nur schwer schnell eine Lösung. Aus dieser Frustration heraus habe ich https://www.stablebuild.com gebaut; es bietet Docker-basierte deterministische Builds auf Ubuntu-, Debian- und Alpine-Containern. Es besteht aus einem unveränderlichen Docker-Hub-Pull-through-Cache, täglichen vollständigen Kopien der Ubuntu-/Debian-/Alpine-Paket-Repositories, täglichen Kopien beliebter PPAs und des PyPI-Index sowie einem unveränderlichen Cache für beliebige Dateien/URLs.
      In der Praxis ist es einfach zu benutzen und zu debuggen und bietet breite Softwarekompatibilität; bei Containern, die wir auf StableBuild umgestellt haben, gab es 0 Probleme aufgrund von Nichtdeterminismus.
    • Die Dokumentation ist nicht nur schwer zugänglich; besonders bei der neuen CLI und flakes ist sie oft einfach falsch, und das sind nicht nur Edge Cases.
      Ich habe einmal Zeit damit verbracht zu verstehen, warum nix develop nicht so funktioniert wie in der Dokumentation beschrieben und wie man es dazu bringt. In der Dokumentation heißt es im Grunde, man gelange in die Build-Time-Umgebung, aber der tatsächliche Standard ist nicht abgeschottet, und auch die Namen der Kommandozeilenoptionen sind verwirrend, sodass ich mir das Wissen aus dem Source erarbeiten musste.
      Ich wünschte mir auch, dass Edge Cases, die die Reproduzierbarkeit brechen, expliziter behandelt würden. Floating-Point-Code reagiert empfindlich auf die Reihenfolge der Operationen, und durch Preemption des Betriebssystems kann Zustand nach außen dringen. Wenn man selbst Offensichtliches nicht klar benennt, schießen sich die Leute am Ende selbst ins Knie.
    • Ganz so schlimm ist es nicht, aber selbst in einer standardmäßigen NixOS-Konfiguration bleibt in Benutzerkonten und im Systeminneren enorm viel nicht reproduzierbarer Zustand zurück.
      Mit der Konfiguration „Erase your darlings“ kann man den meisten nicht reproduzierbaren Zustand außerhalb der Benutzerkonten loswerden. Es ist etwas umständlich, aber was ist unter NixOS schon nicht umständlich?
      https://grahamc.com/blog/erase-your-darlings/
      Den Bereich innerhalb der Benutzerkonten ignoriere ich, und Home Manager mag ich nicht.
    • Bei der Arbeit nutzen wir sowohl Docker als auch NixOS, aber die oben genannten Probleme habe ich noch nie erlebt.
      Docker ist in Ordnung, hat auf dem Mac aber eher schlechte Performance; an Nix gefällt mir, dass es trivial ist, mehrere Maschinen auf dieselbe Weise zu installieren und zum Laufen zu bringen. Die Nix-Dokumentation ist miserabel, aber ChatGPT-4 war hervorragend darin, Nix-Probleme zu lösen.
      Mein Eindruck ist, dass 90 % der Nix-Probleme daraus entstanden, dass man versucht hat, etwas zu tun, das nicht dem „Nix-Weg“ entspricht.
    • Es lohnt sich, einmal eine unveränderliche Distribution wie Fedora Atomic auszuprobieren.
      Ich habe so ziemlich jeden Paketmanager von Distributionen ausprobiert und sie alle irgendwie kaputtbekommen, ohne etwas Besonderes zu tun. Fedora Kinoite hält sich bei mir gut, selbst mit Layer hinzufügen/entfernen, täglichen Updates und sogar Rebase von Silverblue. Persönlich glaube ich, dass rpm-ostree Nix ersetzen wird.
  • Ich sehe Nix und NixOS in einem ähnlichen Zustand wie git vor GitHub
    Die Grundidee basiert auf ernsthafterer Informatik als die bisherigen Ansätze wie SVN oder Docker, und die interne Klempnerei hat zwar noch Probleme, ist aber nicht schlimmer; die nutzerseitigen Tools und die Dokumentation sind jedoch nicht auf dem Niveau für Mainstream-Adoption
    Mit dem Erscheinen von flox könnte sich das geändert haben: https://flox.dev. Es ist nahezu reibungslos, und da es von Leuten mit DE-Shaw-Hintergrund kommt, ist das auch nicht überraschend
    Nix ergibt ohne flakes und nix-command kaum Sinn, aber beides ist als experimentell dokumentiert und standardmäßig deaktiviert. Die Dokumentation wird besser, reicht aber noch nicht aus, und nixlang ist großartig, wenn man es gut lernt, kann als Einstiegshürde für den Mainstream aber nicht akzeptiert werden. nix-env -iA foo ist in der Praxis meistens nicht das Verhalten, das man eigentlich will; daher ist Nix weniger der Paketmanager, als der es beworben wird, sondern eher eine Geheimwaffe für Unternehmen, die sich internes Fachwissen leisten können
    flox senkt die Hürde für „einmal ausprobieren und sofort eine bessere Erfahrung haben“. Nix/NixOS oder etwas Ähnliches wird Docker irgendwann so wie Subversion in den Hintergrund drängen, aber bis zu einem GitHub-ähnlichen Moment wird das nicht passieren — und dann auf einmal
    Die meisten Nix-Beschwerden in diesem Thread sind technisch gesehen falsch, aber völlig nachvollziehbar, und wichtiger noch: Es ist nicht die Schuld der Nutzer. Mir ist klar, dass die Generation, die noch eine Welt ohne git/GitHub kannte, kleiner wird, aber es lohnt sich, Linus dabei zuzuhören, wie er vor Leuten, die die frühen Google-Einstellungskriterien bestanden haben, erklärt, warum sie sich für ein Tool interessieren sollten, das sie als kompliziert empfinden
    https://youtu.be/MjIPv8a0hU8?si=QC0UnHXRdMpp2tI4

    • Hier ist Ron von flox.dev; dieser Beitrag hat unser ganzes Team sehr zum Lachen gebracht
      Wir arbeiten schon seit einiger Zeit daran, und wenn es etwas gibt, das wir priorisieren sollten, um es besser zu machen, würden wir es gern hören
    • Ich glaube, damit ein Developer Tool erfolgreich ist, muss es für die häufigsten Aufgaben mindestens drei Möglichkeiten geben, wie Ingenieure das Tool falsch verwenden können, aber trotzdem mit impliziter technischer Schuld „fertig“ werden
      Bei git ist das so, bei Nix noch nicht; deshalb bin ich mir nicht sicher, ob ein GitHub-ähnlicher Moment helfen würde
  • In diesem Blogbeitrag fehlt eine Erklärung, warum geteilte Docker-Layer nützlich sind
    Der Grund ist Caching: Je mehr Images dieselben Layer teilen, desto mehr kann gecacht werden, wodurch Container schneller starten
    Docker ist darin schwach, weil ein Image beim Bauen möglichst dieselben Layer wie bereits vorhandene Layer erzeugen muss, um vom Caching zu profitieren. Wenn man zum Beispiel heute apt-get install python3 ausführt und es keine neuen Updates gibt, sollte exakt derselbe Layer entstehen wie gestern. Docker-Layer werden jedoch anhand von Datei-Hashes gecacht, daher müssen alle Dateien exakt identisch sein, bis hin zu Metadaten wie Erstellungszeiten
    Nix speichert Abhängigkeiten ohnehin per Hash, daher sind die Layer bei derselben Version und derselben Konfiguration immer identisch

    • So formuliert trifft es wohl besser: Das Dockerfile-Format erzwingt zwischen Layern eine Hierarchie
      Abhängigkeiten bilden normalerweise keinen Baum, sondern einen Graphen, und das wird schnell lästig. Alternative Tools wie Nix oder vermutlich Bazel haben diese Einschränkung nicht und können den Abhängigkeitsgraphen auf Docker-Layer abbilden, um feingranulares Caching zu erreichen. Mit einem Dockerfile lässt sich das so nicht ausdrücken
    • In Docker wird ein Layer mit apt-get nicht automatisch invalidiert, wenn der Layer gecacht ist
      Er ändert sich nur, wenn man --no-cache verwendet oder sich ein übergeordneter Layer geändert hat
  • Ich habe in den letzten 2–3 Tagen damit gekämpft, unter Darwin Docker-Images zu bauen, und dieser Artikel fühlt sich an, als würde das Universum mich verspotten.
    Nix ist für das gewünschte Ziel eindeutig das beste Werkzeug, aber es hat dunkle, verlassene Ecken, die einem die Seele aussaugen. Ich mag es, aber manchmal fühle ich mich wie Morty, der in Ricks Compilerland-Abenteuer hineingezogen wurde.

    • Das große Problem ist das Docker-Design selbst.
      Docker ist im Kern eher ein Gefängnis für Linux-Binärdateien. Unter Linux baut man die Binärdatei, legt sie in den Container, fertig; auch der Nix-Code ist einfach. Wenn man den Code bauen kann, ist das Erzeugen des Containers nur ein weiterer Schritt.
      Docker verlangt jedoch Linux-Binärdateien, und auf dem Mac startet Docker Desktop eine Linux-VM, erledigt die ganze Arbeit darin und versteckt diese Tatsache. Nix macht das nicht, daher gibt es zwei Optionen.
      Erstens Cross-Compilation; dafür muss aber sogar glibc cross-kompilierbar sein, und auch wenn das bei den meisten Community-Abhängigkeiten funktioniert, kann es sein, dass sich bei manchen Paketen die Autoren nicht um Cross-Compilation gekümmert haben. Außerdem führt Hydra, das den Standard-Nix-Cache befüllt, keine Cross-Compilation-Builds aus, sodass man möglicherweise lange baut und dann scheitert.
      Zweitens kann man einen Linux-Builder an den Mac anbinden und x86_64-linux-Build-Jobs dorthin schicken. Das kann eine physische Maschine, eine VM oder sogar ein NixOS-Docker-Container sein.
      Variante 1 wirkt wie der richtige Weg, aber Variante 2 ist praktischer. Das Problem, das du erlebst, liegt vermutlich daran, dass du Variante 1 versuchst; dafür braucht man nicht nur Nix-, sondern auch viel Cross-Compilation-Erfahrung. Es wäre schön, wenn Hydra auch Cross-Compilation von Darwin nach Linux bauen, cachen und Brüche verhindern würde, aber das würde auch die Kosten erhöhen. Es scheint besser, Lösung 2 auszuprobieren.
      Es gab, glaube ich, eine offizielle Lösung: https://ryantm.github.io/nixpkgs/builders/special/darwin-bui...
    • Mit Orbstack funktioniert das tadellos.
      In manchen Stacks ist Cross-Compilation nicht besonders robust, daher kompiliere ich {aarch64,amd64} x {linux,darwin} mit Docker quer. Auf Darwin aarch64 lasse ich mehrere Docker-Instanzen laufen und kompiliere alles damit; die Erfahrung war gut.
    • Meinst du solche 20-Minuten-Abenteuer?
      https://github.com/gytis-ivaskevicius/high-quality-nix-conte...
    • macOS ist definitiv rauer, und dort nutze ich colima, was einigermaßen gut funktioniert.
      Es gibt ein, zwei Bugs, die aber hauptsächlich mit Volumes zu tun zu haben scheinen; Docker-Image-Builds bewältigt es ordentlich. Der rauere Teil ist eher die Geschwindigkeit: Die Kombination aus Hardware und der Tatsache, dass Docker eine Linux-VM emulieren muss, haut ziemlich rein.
  • Die Erfahrung, mit Nix Docker-Images für Java-Anwendungen zu bauen, war nicht besonders gut
    Seit gradle2nix aufgegeben wurde, scheint es keine klare Alternative für Gradle-basierte Java-Anwendungen zu geben. Ich hatte früher einen Freund gebeten, ein möglichst kleines Docker-Image für eine einfache Spring-Boot-Anwendung zu erstellen; das mit Nix erzeugte Ergebnis war doppelt so groß wie das Image ohne Nix
    Der Code ist hier zu sehen: https://github.com/jossephus/Docker_challenge/blob/main/flak...

    • Der Grund ist, dass zwei JDKs enthalten sind
      Es werden sowohl Zulu als auch das JDK mitgeliefert, das Gradle als jdk-Argument einbindet. Wenn man sich gradleGen in nixpkgs ansieht, versteht man, was gemeint ist. Sorry wegen gradle2nix; ich arbeite an einer weniger hackigen Verbesserung
    • Ich habe Java seit über zehn Jahren nicht mehr verwendet und kann daher nicht viel helfen, aber ich habe schon einmal eine Anwendung samt Python, allen Abhängigkeiten, busybox und tini in einen 70-MB-Container gepackt
      Grob sah das so aus: https://gist.github.com/takeda/17b6b645ad4758d5aaf472b84447b...
      Ich habe alles gegen musl gelinkt, beim Kompilieren von Python alle Pakete deaktiviert, die die Anwendung nicht nutzt, und ungenutzte Teile aus boto3/botocore entfernt. Allein boto3/botocore sind schon über 100 MB groß
      Nix-Pakete zielen standardmäßig auf das NixOS-Betriebssystem ab; in üblichen Situationen mit reichlich Speicherplatz möchte man daher, dass alle Funktionen aktiviert sind. Dadurch kommen viele nicht benötigte Abhängigkeiten mit. Alpine-Images hingegen sind für Docker konzipiert, und ihr Ziel ist es, Zusatzfunktionen in Paketen abzuschalten, weshalb das Ergebnis kleiner ist
      Für kleine Images muss man mit override unnötige Dinge deaktivieren. Zulu bringt zum Beispiel alsa, fontconfig, freetype, xorg, cups, gtk, cairo, ffmpeg und Ähnliches mit. Der Freund hat nur die benötigten Dateien sorgfältig extrahiert und in den Container gelegt; bei Nix wurde dagegen im Grunde das komplette Zulu-Paket samt allen Abhängigkeiten gebündelt
      Zuerst sollte man es so anpassen, dass nur ein JDK enthalten ist, und danach kann man mit den oben genannten Methoden die JDK-Größe weiter reduzieren. openjdk_headless zu verwenden könnte noch einfacher sein
      https://github.com/NixOS/nixpkgs/blob/master/pkgs/developmen...
    • Bei minimalen Java-OCI-Containern ist jib schwer zu schlagen
      https://github.com/GoogleContainerTools/jib/tree/master/jib-...
    • openjdk_headless überspringt die GTK- und X-Abhängigkeiten, die Spring nicht benötigt
    • Ich habe selbst an der Challenge teilgenommen und den Nix-Code etwas aufgeräumt; der Kern scheint darin zu liegen, eine sehr kleine JRE zu bauen
      Statt Zulu habe ich auf den headless-OpenJDK-Build aus Nixpkgs umgestellt und so Abhängigkeiten zu GUI-Bibliotheken entfernt; mit pkgs.jre_minimal und jlink habe ich eine maßgeschneiderte minimale JRE erstellt
      Die Image-Größe liegt bei 161 MB und ist damit etwas größer als das demo_jlink-Image. Das liegt daran, dass die JRE etwa 90 MB groß wird, weil sie alle Module enthält, die tatsächlich zum Ausführen der Anwendung nötig sind. Der jdeps-Aufruf in Dockerfile_jlink erkannte nicht alle Module und erzeugte eine JRE nur mit java.base. Wenn ich auch bei meiner minimalen JRE nur java.base einfüge, liegt die JRE-Größe bei etwa 50 MB, und das kaputte Container-Image hat unter Podman 117 MB
      Außerdem habe ich das fehlerhafte copyToRoot im Aufruf von dockerTools.buildImage entfernt. Allein durch den String-Kontext von config.Cmd landet die App bereits im Image; der bisherige Code kopierte die App noch einmal. Zudem habe ich auf dockerTools.buildLayeredImage umgestellt, sodass jeder Store Path in eine eigene Image-Layer kommt. Das ist gut für die Skalierbarkeit des Speicherplatzes, wenn Abhängigkeiten zwischen mehreren Container-Images geteilt werden, wirkt sich aber auf ein einzelnes Image-Experiment nicht aus
      Übrig bleibt vor allem die Optimierung der JRE-Größe. glibc ist mit etwa 30 MB die nächstgrößere Abhängigkeit; vermutlich, weil Nixpkgs glibc mit Unterstützung für viele Locales und Zeichencodierungen baut. Ob es möglich und praktikabel ist, das in eine separate Derivation oder einen separaten Output auszulagern und auswählbar zu machen, weiß ich nicht. Wenn man mehrere Images mit dockerTools.buildLayeredImage baut, werden glibc und die übrigen Abhängigkeiten geteilt, vorausgesetzt, sie verwenden denselben Nixpkgs-Commit
      https://github.com/max-privatevoid/hackernews-docker-challen...
  • Wenn ihr Nix bereits eingeführt habt, ist das großartig; es wäre schön, wenn sich deklarative Paketverwaltung wie Nix oder Guix stärker verbreiten würde.
    Wenn ihr bereits Docker nutzt, Nix aber schrittweise einführen wollt, gibt es auch den Ansatz aus diesem Vortrag: https://youtu.be/l17oRkhgqHE
    Statt Konfiguration und Container-Builds sofort komplett nach Nix zu verlagern, kann man das Dockerfile beibehalten und daraus die Nix-Konfiguration bauen lassen. Der größte Nachteil ist, dass man Layer überhaupt nicht nutzen kann; der Vorteil ist, dass man das Dockerfile schrittweise ändern und bestehende Docker-Infrastruktur oder -Automatisierung wiederverwenden kann.

    • Dieser Artikel verwendet denselben Ansatz.
      [1] https://mitchellh.com/writing/nix-with-dockerfiles
    • Eine Stoßrichtung des Artikels ist, dass Docker-Builds nicht reproduzierbar sind, Nix dagegen schon.
      Allerdings scheint ein großer Teil dieser Nicht-Reproduzierbarkeit daher zu kommen, dass nicht garantiert ist, dass einer der Docker-Layer dauerhaft verfügbar bleibt. Dann frage ich mich, ob Nix garantiert, dass Paketversionen für immer im Repository bleiben.
  • Etwas off-topic, aber die Illustratorin Annie Ruygt, die die Zeichnungen in diesen Folien erstellt hat, hat auch die Logos und Brand Art für zwei YC-Startups gemacht: RethinkDB (rethinkdb.com) und Pachyderm (pachyderm.io).
    Sie hat bei Pachyderm gearbeitet, und Pachyderm wurde von ehemaligen RethinkDB-Ingenieuren gegründet. Ihre Arbeiten sind wirklich gut.

  • Vor relativ kurzer Zeit habe ich auf Empfehlung des Infrastruktur-Teams etwa einen halben Tag damit verbracht, mit Nix ein CI-Basis-Image zu bauen. Das Image war riesig, und wegen Link-Problemen funktionierten Teile davon nicht.
    Besonders nervig war, dass es beim Erstellen von Multi-Architektur-Images tatsächlich versucht, etwas für eine andere Architektur auszuführen, und nur qemu-Hardwarevirtualisierung unterstützt. Build-Maschinen und Workstations sind VMs, daher gibt es das dort nicht; binfmt-misc ist aber vorhanden. Wenn es ein arm64-mkdir geforkt/exec’t hätte, um /tmp anzulegen, hätte es funktioniert. Docker-Layer sind aber nur tar-Dateien, also kann man ein Verzeichnis wie folgt erstellen:
    echo "tmp uid=0 gid=0 time=0 mode=0755 type=dir" | bsdtar -cf - @-
    Genau dieser Layer existiert mit hoher Wahrscheinlichkeit schon irgendwo, sodass Nutzer ihn womöglich nicht einmal herunterladen müssten.
    Jedes Mal, wenn ich Nix ausprobiere, habe ich das Gefühl, dass es nur noch ein paar Monate braucht, bis ich es regelmäßig nutzen kann. In nixpkgs gibt es fast alle Pakete, die ich möchte, und auf der Workstation lässt es sich gut installieren. Aber die Anforderung „ich brauche bash, python, build-essential und Bazel“ wirkt nicht wie das Ziel eines Docker-Image-Builders. Wenn man nur ein einzelnes Go-Binary in ein Docker-Image packen will, braucht man Nix nicht. Man nimmt distroless und legt die Anwendung in eine tar-Datei, dann ist das ein Container. Persönlich nutze ich Bazels rules_oci; intern macht es im Grunde genau das und bringt nur noch so viel Intelligenz mit, dass es Binaries für mehrere Architekturen baut, ein Image-Index-YAML erzeugt und es in eine Registry pusht.

    • Binaries für andere Architekturen sollte man per Cross-Compilation bauen können, ohne sie tatsächlich auszuführen.
      Natürlich müssen die Build-Dateien des Pakets das unterstützen. Auch die Aussage, qemu unterstütze nur Hardwarevirtualisierung, scheint nicht zu stimmen. qemu kann auch für Architekturen genutzt werden, für die nur Software-Emulation möglich ist.
      Ein Minimalbeispiel wird hier behandelt: https://discourse.nixos.org/t/how-do-i-get-a-shell-nix-with-...
      Ich würde nicht behaupten, dass es so einfach sein müsste wie die Nutzung von pkgCross, aber mich würde interessieren, auf welches konkrete Problem du im üblichen Ablauf gestoßen bist.
      https://nix.dev/tutorials/cross-compilation.html
    • Gemeint ist vermutlich das offizielle Nix-Docker-Image, und das ist tatsächlich groß.
      Ich habe es seit Jahren in mehreren Projekten verwendet, aber wenn die Größe ein Problem ist, kann man mit der im Artikel beschriebenen Methode sehr kleine Images erstellen, die nur das enthalten, was man angegeben hat.
      [1] https://hub.docker.com/r/nixos/nix/tags
    • Cross-Compilation in Docker-Images war genau der Zweck, für den ich Nix verwendet habe.
      Ich habe sogar musl genutzt; damit wurden in CI schneller und konsistenter als mit jedem anderen Tool die kleinsten Images gebaut, und das Caching funktionierte ebenfalls gut. Ich verstehe nicht ganz, was da ausgeführt worden sein soll.
    • Mich würde interessieren, wie die endgültigen Nix- und Docker-Dateien aussahen und ob buildFHSEnv nötig war, um irgendwelche seltsamen Third-Party-Binaries zu unterstützen.
      Nix scheint mehr Artikel zu brauchen, die erklären, wie man sanft und stückweise von bestehenden Systemen migriert.
  • Als Plattform-Engineer würde ich Nix gern mögen, aber für alle anderen ist es nicht einfach
    Auch die Developer Experience finde ich noch ziemlich schlecht. Zum Beispiel bevorzuge ich die Developer Experience von devbox, weil man Pakete mit etwas wie devbox add python@3.11 hinzufügen kann
    Wenn man sich eine 120 Zeilen lange flake.nix ansieht, kann man schwer behaupten, das sei „einfacher“
    https://github.com/Xe/douglas-adams-quotes/blob/main/flake.n...

    • Der Vergleich ist etwas unfair
      Die verlinkte Flake definiert, wie ein Go-Binary gebaut wird, definiert ein Docker-Image, das dieses Binary als Entry Point verwendet, und definiert außerdem ein NixOS-Modul, das einen systemd-Service erstellt, der das Go-Binary ausführt. Außerdem enthält sie sogar einen NixOS-Test, der prüft, ob dieses NixOS-Modul wie erwartet einen systemd-Service erstellt
      Das NixOS-Testframework ist ziemlich beeindruckend, und die Tests laufen in einer QEMU-VM, in der NixOS mitgestartet wird. Für den verlinkten Artikel relevant sind nur die Definitionen für das Go-Binary und das Docker-Image sowie ein Teil des umgebenden Boilerplates
    • Diese 120 Zeilen taugen kaum als repräsentatives Beispiel; bei einem einzelnen Service hätte man das vermutlich inline geschrieben, ohne ein neues Modul zu erstellen
      Außerdem entsprechen viele Zeilen 1:1 einer inline beschriebenen systemd-Service-Definition und würden unabhängig vom verwendeten System nicht verschwinden. Es ist auch eine elegante Methode enthalten, mehrere Systeme per Override zu deklarieren
      Dieses Beispiel ist eher „lass uns eine triviale Sache so machen, als wäre sie ein großes, ernsthaftes Projekt“; wenn man es als Einmalfall behandelt, ließe es sich vermutlich auf etwa 40 Zeilen reduzieren
    • Diese 120 Zeilen erledigen ziemlich viel
    • Für ein einzelnes Binary und eine systemd-Konfiguration fallen 120 Zeilen Code an
      Dieses Binary und die Konfigurationsdatei sind so ziemlich das Nächstliegende an einem „flat pack“, das man unter Linux bekommen kann; dieses Beispiel zeigt gut, was wir zwischen Wald und Bäumen übersehen haben
  • Auch Guix hat mit guix pack -f docker eine einfache und ordentliche Docker-Option
    Guix hat außerdem den Vorteil, dass es statt einer eigenen Spezialsprache eine bereits verwendete Sprache nutzt: Guile/Scheme
    [1] https://guix.gnu.org/manual/en/html_node/Invoking-guix-pack....