- Railway hat mit Railpack ein neues Build-System als Ersatz für Nixpacks vorgestellt
- Railpack bietet gegenüber Nixpacks Vorteile wie feingranulares Versionsmanagement, kleinere Image-Größen und verbessertes Caching
- Das commit-basierte Versionsmodell von Nixpacks stieß bei unterschiedlichen Nutzeranforderungen und der Skalierbarkeit an Grenzen
- Railpack verbessert Stabilität und Flexibilität der Build-Umgebung durch BuildKit-Integration, Schutz geheimer Umgebungsvariablen und Unterstützung für verschiedene Sprachen und Frameworks
- Derzeit werden Node, Python, Go, PHP und statisches HTML unterstützt; die Unterstützung für weitere Frameworks und Sprachen wird laufend ausgebaut
Überblick und Hintergrund
- Railway hat mit Railpack ein Build-System der nächsten Generation vorgestellt
- Railpack ist ein neues Tool, das auf den Erfahrungen basiert, die Railway beim Build von mehr als 14 Millionen Apps auf der Plattform mit Nixpacks gesammelt hat
- Nixpacks war für 80 % aller Nutzer geeignet, doch mehr als 200.000 Nutzer stießen auf Einschränkungen und hatten dadurch Probleme
- Für den Ausbau der Nutzerbasis und eine nachhaltige Build-Umgebung hielt man ein großes Upgrade für notwendig
Zentrale Verbesserungen von Railpack
- Feingranulares Versionsmanagement: Für jedes Paket werden präzise Versionsangaben im Format
major.minor.patch unterstützt, wodurch die Grenzen des unklaren Versionsmodells von Nix überwunden werden
- Kleinere Image-Größen: Die Standard-Build-Images für Node wurden um 38 % und für Python um bis zu 77 % verkleinert, was schnellere Deployments ermöglicht
- Verbessertes Caching: Durch die direkte Integration mit BuildKit können Layer und Dateisystem gesteuert, Cache-Treffer verbessert und Caches zwischen Umgebungen geteilt werden
- Railpack-Builds werden bereits auf railway.com und in zentralen Diensten eingesetzt
Probleme bei der Nutzung von Nixpacks
- Das Paket-Versionsmanagement von Nix basiert auf Commits, bietet nur die jeweils aktuelle Major-Version an, und jede Version entspricht einem bestimmten Commit im nixpkgs-Repository
- Es ist ineffizient, selbst kleine Patch-Versionen manuell verwalten zu müssen; auch für Mitwirkende ist das Versionsmodell wenig intuitiv, was die Zugänglichkeit senkt
- Selbst bei Sprachen wie Node oder Python wird letztlich nur die neueste Major-Version unterstützt
- Bei Versionsupdates wirkt sich eine Änderung des Commit-Hashs gleichzeitig auf andere Paketversionen aus, was die Zuverlässigkeit für Nutzer senkt und zu unerwarteten Build-Fehlern führen kann
- Bei Nixpacks liegen alle Abhängigkeiten in einem einzigen Layer unter
/nix/store, wodurch sich Images nur schwer sinnvoll aufteilen oder verkleinern lassen
- Auch das Caching ist problematisch, da bei jeder Injektion von Umgebungsvariablen Layer stets invalidiert werden und der Cache nicht effektiv genutzt werden kann
Nicht ein Problem von Nix selbst, sondern der Art der Nutzung
- Das Problem liegt nicht im Design von Nix selbst, sondern in der Art, wie Railway es verwendet und abstrahiert hat
- Man wollte ein System entwerfen, bei dem Nutzer weder das Derivation-Konzept von Nix noch die interne Versionsstruktur verstehen müssen, kam aber zu dem Schluss, dass dies praktisch nicht möglich ist
- Um diese Probleme zu lösen, wurde Railpack entwickelt
Die technische Architektur von Railpack
- Wechsel der Codebasis von Rust zu Go: Um BuildKit besser zu nutzen und stärker an das Ökosystem anzuschließen, wurde auf Go umgestellt
- BuildKit LLB und Frontend: Durch die direkte Erzeugung eines eigenen BuildKit-LLB und Frontends kann die Struktur der Build-Images präzise kontrolliert werden → die Standard-Images für Node und Python sind dadurch deutlich schlanker als bei Nixpacks
- Versionsmanagement mit Mise: Für Paketinstallation und Versionsauflösung wird Mise verwendet; künftig lassen sich damit auch andere Quellen für ausführbare Dateien leichter unterstützen
- Bei erfolgreichen Builds wird der Zustand der Abhängigkeiten festgeschrieben → selbst wenn sich die Standard-Node-Version von 22 auf 24 ändert, gehen bestehende Builds nicht kaputt
- Mithilfe der Secret-Funktion von BuildKit werden Sicherheit und Verwaltung von Umgebungsvariablen verbessert
Build-Schritte in Railpack
- Analyze: Analyse des Codes, um benötigte Pakete, Ausführungsbefehle und Startkommandos zu ermitteln
- Plan: Erstellung eines Build-Plans in JSON-serialisierbarer Form (mit mehreren Stufen; jede Stufe hängt vom Ergebnis vorheriger Stufen oder vom gesamten Image ab)
- Generates: Erzeugung des Build-Graphen von BuildKit (auf Basis von Ein- und Ausgaben)
Strategische Builds mit BuildKit
- Während Dockerfiles seriell arbeiten, verarbeitet BuildKit mehrere Befehle parallel und erlaubt eine feinere Kontrolle von Ein- und Ausgaben je Schritt
- Railpack definiert auf Grundlage der Codeanalyse alle Build-Schritte und beschreibt deren Abhängigkeiten detailliert auf niedriger Ebene
- Dieser Plan wird in einen BuildKit-LLB-Graphen umgewandelt und aufgelöst
- Wenn sich Umgebungsvariablen oder andere Werte ändern, werden Dateien anhand des Hash-Werts dieses Werts gemountet; wenn sich weder Code noch Variablen ändern, sind Cache-Treffer garantiert
- Dadurch kann Railpack die Art und Weise der Image-Erzeugung vollständig kontrollieren
Neue Funktionen durch die Einführung von Railpack
- Build und Deployment statischer Sites mit Vite, Astro, CRA und Angular werden ohne Konfiguration unterstützt
- Enge Integration des Build-Prozesses in die Railway UI
- Unterstützung für neueste Sprachversionen ist möglich, ohne dass dafür ein eigenes Railpack-Release nötig ist
- Optimiertes Caching zwischen Umgebungen auf Projektebene
- Derzeit werden Node, Python, Go, PHP und statisches HTML unterstützt; die Unterstützung für Frameworks und Sprachen wird laufend erweitert
Open Source und Zukunftspläne
- Railpack ist derzeit als Beta öffentlich verfügbar und kann nach der Aktivierung sofort genutzt werden
- Offizielle Dokumentation, der tatsächliche Code und öffentliche Support-Kanäle werden auf railpack.com bereitgestellt
- Künftig liegt der Fokus zunächst auf tiefer Unterstützung für weit verbreitete Sprachen; nach der Etablierung von Core-API und Abstraktionsniveau soll der Umfang weiter ausgebaut werden
1 Kommentare
Hacker-News-Kommentare
Ich bin ein Nix-Fan, aber ich hoffe, man glaubt mir, dass ich nicht emotional daran hänge, dass man sich gegen Nix entschieden hat. Einige der Kritikpunkte in diesem Artikel verstehe ich jedoch nicht ganz und finde, sie bräuchten mehr Erklärung. Zum Beispiel heißt es: „Das größte Problem von Nix ist die commitbasierte Paketversionierung.“ Nixpkgs ist eine großartige Ressource, aber Nix und Nixpkgs sind nicht dasselbe. Wenn man beliebige Versionen einer Toolchain holen will, ist Nixpkgs dafür tatsächlich eher ungeeignet, aber mit Nix gibt es auch andere Wege. Gerade für Rust gibt es z. B. wirklich gute Nix-Tools, die beliebige Versionen sauber bereitstellen. Auch die Aussage „Nix-Abhängigkeiten lassen sich nicht in separate Layer aufteilen“ ergibt für mich überhaupt keinen Sinn. Man kann sie aufteilen, wie man möchte. Auch die Docker-Tools von Nixpkgs unterstützen das. Der Teil, in dem die Codebasis von Rust auf Go umgestellt wurde, hat zwar nichts direkt mit Nix zu tun, ist aber interessant. Normalerweise entscheidet man sich nicht leichtfertig für einen Sprachwechsel; meist passiert so etwas eher, wenn man ohnehin einen Neustart plant. Ich vermute, dass Railpacks und Nixpacks nicht von denselben Leuten gebaut wurden. Ich habe auch schon gesehen, was passiert, wenn Leute ohne tiefes Nix-Verständnis in einer Organisation mit einer unfertigen Nix-Lösung umgehen müssen. Das sieht selten gut aus, und die meisten Menschen wollen Nix nicht lernen. Deshalb wird Nix an meinem eigentlichen Arbeitsplatz fast gar nicht verwendet, um genau solche Situationen zu vermeiden
Ich arbeite gern mit Nix, aber es ist extrem frustrierend, dass man bei jeder Diskussion über grundlegende Benutzungsprobleme von Nix nur Antworten bekommt wie „Dafür gibt es einen Workaround“ — der dann schlecht dokumentiert ist, eine merkwürdige Sprache nutzt, schlechte Fehlermeldungen hat und oft Dutzende oder Hunderte zusätzliche Zeilen Code erfordert, zusammen mit Insiderwissen von Leuten, die es schon einmal gemacht haben. Die meisten Probleme rund um Nix kommen nicht von Turing-Vollständigkeit, sondern daher, dass grundlegende Dinge wie intuitive APIs als Built-ins fehlen. Wenn die Nutzung von Nix in jedem Projekt immer mehr dazu führt, dass man sich hauptsächlich mit den Problemen von Nix selbst beschäftigt, gibt es wenig Grund, Nix statt gut dokumentierter Mainstream-Tools zu verwenden. In der Praxis entscheiden sich die meisten Leute genau deshalb für Docker. Ich finde es sehr enttäuschend, dass Nix auf idealistischer Reinheit beharrt, statt die realen Probleme der Developer Experience in realistischer Zeit zu lösen. Natürlich tragen alle freiwillig dazu bei, aber es ist wirklich schade zu sehen, wie so viel technische Arbeit am Ende wegen schlecht gestaltetem UX praktisch unbenutzbar bleibt
Ich nutze Nix nicht, aber die Behauptung „Nix ≠ Nixpkgs“ wirkt auf mich etwas realitätsfern. Für die meisten Nutzer ist Nixpkgs am Ende eben Nix selbst, wenn die Alternativen zusätzliche Recherche und zusätzlichen Aufwand erfordern. Auch bei „man kann es in separate Layer aufteilen“ würde mich interessieren, ob das wirklich intuitiv, einfach und das Standardverhalten ist
Entscheidend ist, dass die Nutzer von Railway Entwickler sind, die die Versionen ihrer gewünschten Pakete selbst festlegen wollen. Aufgrund der Struktur von Nix und Nixpkgs bedeutet das Fixieren einer Paketversion, dass man den Commit des gesamten nixpkgs-Baums festlegt. Da node/python/ruby-Paket-Builds oft von Dingen außerhalb dieses Baums abhängen, braucht man ein Mapping zwischen Versionen und Commits. Diese Abstraktion ist nicht perfekt, sodass ein Nutzer unter Umständen selbst dann den Zustand des Baums anpassen muss, wenn er einfach nur „yarn add Paket“ machen will. Nix ohne Nixpkgs zu verwenden, ist für begrenzte Einsatzzwecke in Ordnung, aber für eine Plattform wie Railway eine schwierige Entscheidung
Ich verstehe die Kontroverse um die Versionierung nicht ganz. Ich nutze Nix gerade erst zum ersten Mal, aber ich habe eindeutig Pakete aus einem bestimmten Commit
Ich finde, das wurde gut herausgearbeitet. Nixpkgs und Nix sind zwar verschieden, aber faktisch ist Nixpkgs der eigentliche Vorteil. Mit NixOS habe ich zum ersten Mal die neueste Linux-Kernel-Version direkt am Releasetag benutzt. Debian Stable ist auch okay, fühlt sich aber immer so an, als würde man ein paar Jahre in die Vergangenheit zurückgehen. Die Nix-Sprache ist allerdings aus guten Gründen kritikwürdig. Sie ist alt, und obwohl man damit das Beste gemacht hat, sehe ich keinen Grund, sie unbedingt zu ändern. Das Nix-Build-System wirkt klassisch und baut unnötig viel neu. Wenn man z. B. bei einem NixOS-Installations-ISO nur eine einzige Kernel-Command-Line-Option ändert, etwa die Geschwindigkeit eines Konsolenports, kann es zu dem absurden Effekt kommen, dass der Build trotzdem rund drei Minuten dauert. Das ist irgendwie lustig, aber kein Grund für mich, Nix aufzugeben. In meinem eigenen Build-System würde ich so etwas allerdings niemals akzeptieren. Nix zum Bauen von Docker-Images zu verwenden, halte ich persönlich für den schlimmsten Anwendungsfall. Ich wollte einmal nur die
pg_dump-Binary von Postgres zu einer in Go geschriebenen Binary hinzufügen. Das Infrastrukturteam empfahl Nix, also habe ich es ausprobiert — und aus einer komprimierten 50-MB-Go-Binary wurde ein 1,5-GB-Monster-Image.pg_dumpselbst ist nur 464 KB groß. Am Ende habe ich es mit Bazel,rules_debianunddistrolessdeutlich sauberer gelöst. Bei den meisten Nix-Systemen wirken 1,4 GB wie der Standardwert. Auch beim Bauen großer C++-Projekte ist Nix nicht außergewöhnlich stark. Systeme für den Build der eigenen Software passen oft besser zu den jeweiligen Anforderungen. Ich mag Bazel, und bei Go-Projekten würde ich am liebsten einfach nurgo buildverwenden. In 99 % der Fälle würde ich solche Tools statt Nix nutzen; höchstens für Aktualisierung oder Deployment könnte man ein flake schreiben und es mit home-manager verwendenDie Wahl der Versionierungsstrategie wirkt seltsam. Eine nixpkgs-Version ergibt eindeutig Sinn, wenn man ein System betreibt oder baut. Für eine Plattform, die Runtime/Compiler bereitstellt, müsste man Versionen eher direkt anbieten, ähnlich wie devenv. Zum Beispiel bietet nixpkgs-python „alle Python-Versionen, stündlich mit Nix aktualisiert“. Auch dass Railway bei jedem Build eine Deployment-ID-Umgebungsvariable injiziert, hätte man in einem Layer nach der Installation machen können. Pakete lassen sich ebenfalls auf mehrere Layer verteilen, und auch die automatische Steuerung der Layer-Anzahl ist möglich
Aus Sicht von DevOps/SRE sehe ich meist, dass Versuche, ein Dependency-Management-System zu bauen, in eine von zwei Richtungen gehen (z. B. bei Python). Option 1: „Monorepo + gemeinsame Umgebung“. Vorteile: leicht zu verwalten, einfache Security-Patches, Zentralisierung. Nachteile: Irgendjemand will immer eine spezielle Version, stufenweise Rollouts sind schwierig, schlanke Images ebenfalls. Option 2: „Jeder mit eigenem conda/venv“. Vorteile: individuelle Anpassung, unnötige Pakete können weggelassen werden, schrittweise Upgrades sind möglich. Nachteile: zu viele Umgebungen, keine validierte gegenseitige Kompatibilität, Security-Management wird zum Albtraum. Mit wachsender Berufserfahrung merkt man immer stärker: Es gibt keine Lösungen, nur Trade-offs
„Mit Nix selbst ist nichts falsch. Das Problem war die Art der Nutzung“ ist für mich ein gutes Beispiel für „das richtige Tool für den richtigen Zweck“. Nix ist in manchen Bereichen großartig, in anderen absolut schrecklich. Das Problem ist, dass es so lange dauert, es zu lernen, dass man, wenn man kompetent genug ist, um gute Entscheidungen zu treffen, schon so viel Zeit investiert hat, dass man nur ungern den Kurs ändert — und am Ende Nix mit Gewalt weiter für den ursprünglichen Zweck einsetzt
shell.nixoderconfiguration.nixpassend zu einer Spezifikation generieren. Ich selbst erstelle auch oft komplett enthaltene Umgebungen pro Repository, und mit flakes könnte man noch reproduzierbarere Umgebungen bauen. (flake.nixist ähnlich wieshell.nix, unterstützt aber zusätzlich das Fixieren von Versionen …)Das wirkt so, als würde man künstlich Versionierung in etwas hineinpressen, das eigentlich keine hat. Abhängigkeiten gehen wegen einer „Default-Version“ kaputt? Das ist wie Docker mit dem Tag
:latestzu verwenden und sich dann zu wundern, dass der Server bei jeder Änderung kaputtgeht. Der Blogpost ergibt für mich nicht besonders viel Sinn. Auch bei „Nix-Abhängigkeiten lassen sich nicht in separate Layer aufteilen“ kann ich nicht zustimmen. Man kann/nix/storebeliebig aufteilen, und es wirkt, als sei dort auch nicht ganz klar, wie man Container und Nix zusammen verwenden sollte. Wenn die Fähigkeiten auf diesem Niveau liegen, wird die vorgeschlagene Alternative wahrscheinlich am Ende dieselben Probleme wiederholen. Das ist ein klassisches Beispiel für NIH-SyndromNix dort nicht einzusetzen, wo es nicht passt, ist völlig nachvollziehbar. Aber ein bereits funktionierendes System von Grund auf neu zu bauen, obwohl andere genau diese Probleme längst gelöst haben und man das mit ein wenig Recherche herausfinden könnte, wirkt grundsätzlich seltsam.
nix2containeroder flakes könnten vermutlich alle Probleme lösen. Auch die Versionsverwaltung: Flakes, die ich vor drei Jahren geschrieben habe, bauen heute noch identisch und liefern unveränderte Ergebnisse. Irgendwie riecht das nach einem Plattformwechsel mit Blick auf Go-to-Market oder Fundraising. Ich habe mir übrigens das GitHub-Repository von nixpacks angesehen; dort wird nurrustPlatformverwendet, und wenn es um Rust-Probleme geht, ist rust-overlay praktisch die StandardantwortWenn man darüber nachdenkt, welcher Ansatz sich leichter gegenüber VCs verkaufen lässt, ist der Titel „Deployment-Plattform“ wahrscheinlich attraktiver als ein Nix-Wrapper
Entgegen der Behauptung „Nix-Abhängigkeiten lassen sich nicht in separate Layer aufteilen“ ermöglicht nix2container genau diese Aufteilung. Wenn man z. B. ein Image mit bash braucht, kann man einen separaten Layer nur mit bash bauen, und dieser Layer muss nur dann neu gebaut oder gepusht werden, wenn sich bash ändert. Auch die Behauptung „Durch Abhängigkeiten entsteht ein riesiges Image als einzelner
/nix/store-Layer“ trifft zwar auf die Funktionnixpkgs.dockerTools.buildImagezu, aber nicht aufnix2containerodernixpkgs.dockerTools.streamLayeredImage. Diese Tools erzeugen in der Praxis ein Skript, über das das Image gepusht wird.nix2containererstellt JSON mit den Pfaden aller Layer und nutzt Skopeo, um das Image zu Docker, in Registries, zu podman usw. zu pushen. (Zur Einordnung: Ich bin der Autor von nix2container)Ich möchte wirklich Danke für nix2container sagen. Ich nutze es für Deployments nach AWS (ECR), und die Umschaltzeit zwischen Builds ist auf einstellige Sekunden gesunken
Wir wollten wegen der Größe unserer Docker-Images ohnehin
nix2containertesten. Danke für das tolle ToolIch glaube, das Kernproblem hier ist die Haltung, die von Sprach-Paketmanagern geförderte „Suppe aus Wunschversionen“ unbedingt beibehalten zu wollen — und dieser Ansatz ist nicht nachhaltig. Die vorgeschlagene Alternative Mise versteht Versionsbeschränkungen zwischen Paketen nicht und testet die einzelnen Pakete überhaupt nicht. Man kann daher niemals dieselbe Zuverlässigkeit erwarten
Dass diese „Suppe aus Wunschversionen“ nicht nachhaltig ist, stimmt zwar, aber die Leute nutzen sie weiter, weil sie in der Praxis gut funktioniert. Bibliotheken auf OS-Ebene werden sehr konservativ gepflegt und brechen deshalb nicht so leicht, und mit Tools wie mise oder asdf kann man darauf meist problemlos eigene Versionskombinationen aufsetzen. Und wenn doch etwas kaputtgeht, lässt es sich fast immer schnell durch Anpassen von Version oder Konfiguration beheben. Kaputte Dinge sind zwar lästig, aber oft nicht kritisch. Systeme, die zusätzliche Lernkurven oder zusätzlichen Aufwand verlangen, gelten dagegen schnell als Zeitverschwendung. Menschen, denen ein Zustand wichtiger ist, der „nicht kaputtgeht“, bevorzugen dagegen eher Nix, selbst wenn es unbequemer ist und eine Lernkurve hat. Für einen Anbieter wie Railway mit vielen Nutzern bedeutet das letztlich, stärker auf die erste Gruppe zu achten — auf Einfachheit und Gewohnheit
Ich frage mich, was genau mit „Suppe aus Wunschversionen“ gemeint ist und was die Alternative wäre
Beides ist völlig machbar. Rust-Pakete kann man zum Beispiel anhand der Informationen in
Cargo.lockproblemlos mit Nix bauen. Nixpkgs steht individuellen Versionskombinationen zwar entgegen, aber Nix selbst kann damit gut umgehenNix garantiert keine beliebigen Versionen, sondern Commit-Stände. Bei Edge Cases wie Änderungen an
glibcoder Konflikten mit Shared Libraries kann das mühsam werden. Wahrscheinlich ist es dafür inzwischen zu spät, aber ich könnte auch zu eleganteren Wegen beraten, so etwas mit Nix umzusetzen. Das Produkt selbst finde ich coolNix verhindert Konflikte mit Shared Libraries sogar sehr zuverlässig. Aber schon kleine Änderungen — Kommentare, Dokumentation usw. — führen dazu, dass alle betroffenen transitive Abhängigkeiten komplett neu gebaut werden. Das kann zu gewaltigen Rebuilds führen und die Entwicklung schmerzhaft machen. Das sieht man auch am Staging-Prozess in nixpkgs
Ich verstehe den Wert von Nix sehr gut. Ich finde nur, dass „es geht kaputt“ etwas übertrieben ist. Im Vergleich zu Nix verliert man zwar einige starke Garantien, aber trotzdem läuft es wahrscheinlich noch immer wesentlich zuverlässiger als die meiste andere Software
Ich verstehe nicht, warum man unbedingt an einem nixpkgs-Hash hängen wollte, statt eigene derivations zu bauen
Ich fand es interessant, wie viele Kommentare im Grunde sagen: „Eigentlich lässt sich alles mit Nix lösen — aber nur, wenn man Experte wie ich ist“
Wenn ein Unternehmen seine gesamte Technik und sein Business in JavaScript abbildet, dann aber wegen fehlendem Verständnis bestehender Kernkonzepte wie Funktionen oder Arrays NIH betreibt und eine eigene Sprache entwickelt, dann klingt das eher nach internen Defiziten als nach einem Problem der Technologie selbst
Das ist immer wieder die übliche Stimmung, sobald es um Nix geht
Genau das ist die typische Nix-Stimmung. Dieses Narrativ von „Ich werde die Welt retten“ und die Reaktion auf „Die Funktion, die ich brauche, geht nicht“ mit „Dann benutzt du es eben falsch“ kommt dort ständig zurück