Nix statt Docker: Entwicklungsumgebungen neu gedacht
Zurück zum Blog

Nix statt Docker: Entwicklungsumgebungen neu gedacht

6 Min. Lesezeit
Lesen auf English

Die zwei Gesichter von Docker

Docker hat die Art, wie wir Software deployen, grundlegend verändert. In unserem Beitrag zu Jib und Container Images haben wir beschrieben, wie wir Container-Images für unsere Spring-Boot-Services bauen, und unsere Produktionsworkloads laufen selbstverständlich als Container auf Kubernetes. Daran ändern wir nichts.

Aber Docker hat ein zweites Gesicht, das weniger mit Deployment zu tun hat: Docker Compose als lokale Entwicklungsumgebung. Datenbanken, Message Broker, Caches -- all das per docker compose up hochfahren, damit man lokal entwickeln kann. Und auch die Entwicklungswerkzeuge selbst -- Java, Gradle, Node.js -- werden oft in Devcontainern oder Docker-basierten Setups bereitgestellt, damit "bei jedem die gleiche Version läuft".

Wir haben das jahrelang so gemacht. Und dann haben wir aufgehört.

Das Problem mit Docker Compose für Development

Docker Compose für externe Services -- eine PostgreSQL-Datenbank, ein Redis, ein RabbitMQ -- funktioniert gut. Da haben wir auch nichts dagegen. Das Problem beginnt, wenn Docker Compose zur universellen Antwort auf die Frage wird: "Wie stelle ich sicher, dass alle im Team die gleiche Entwicklungsumgebung haben?"

Die Praxis sah bei uns so aus: Eine docker-compose.yml mit allen Services, dazu gelegentlich Devcontainer-Konfigurationen oder Dockerfiles für das Build-Tooling. Das funktionierte, hatte aber spürbare Nachteile.

Ressourcenverbrauch. Docker Desktop auf macOS ist nicht leichtgewichtig. Die Linux-VM, die im Hintergrund läuft, verbraucht RAM und CPU -- permanent, nicht nur während man Builds ausführt. Bei Projekten mit mehreren Services summiert sich das schnell.

Startup-Zeit. docker compose up braucht Zeit. Images müssen gepullt werden, Container müssen starten, Health-Checks müssen durchlaufen. Bei komplexeren Setups vergehen leicht 30 bis 60 Sekunden, bis man arbeiten kann.

Daemon-Abhängigkeit. Ohne laufenden Docker Daemon geht nichts. Docker Desktop muss gestartet sein, der Daemon muss laufen. Das klingt trivial, ist aber ein weiterer beweglicher Teil im Setup.

Versionspinning ist umständlich. Welche Java-Version braucht dieses Projekt? Welche Gradle-Version? Welche Node-Version für das Frontend? Das alles in Docker Compose oder Devcontainern abzubilden, erzeugt Konfigurationsaufwand, der nichts mit dem eigentlichen Projekt zu tun hat.

Nix devShells: Eine andere Antwort

Wir nutzen NixOS seit 2022 als Host-Betriebssystem für unsere Server, wie wir in unseren Beiträgen zu NixOS als Server-OS und reproduzierbaren Server-Konfigurationen mit Flakes beschrieben haben. Nix Flakes haben sich dort als Grundlage für reproduzierbare Infrastruktur bewährt. Irgendwann war der nächste Schritt naheliegend: Wenn Flakes Server-Konfigurationen reproduzierbar machen können, warum nicht auch Entwicklungsumgebungen?

Die Antwort heißt devShells. Ein Nix Flake kann neben Server-Konfigurationen auch Entwicklungsumgebungen definieren -- eine Shell mit exakt den Werkzeugen und Versionen, die ein Projekt braucht. Keine Container, kein Daemon, keine VM. Einfach eine Shell, in der die richtigen Tools verfügbar sind.

Für ein typisches Java-Projekt bei uns sieht die flake.nix so aus:

{
  description = "my-service - Entwicklungsumgebung";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = import nixpkgs { inherit system; };
      in
      {
        devShells.default = pkgs.mkShell {
          buildInputs = with pkgs; [
            # Java-Toolchain
            temurin-bin-21
            gradle

            # Weitere Werkzeuge
            kubectl
            k9s
            jq
            httpie

            # Datenbank-Client
            postgresql_16
          ];

          shellHook = ''
            export JAVA_HOME=${pkgs.temurin-bin-21}/lib/openjdk
            echo "Java: $(java --version | head -1)"
            echo "Gradle: $(gradle --version | grep '^Gradle')"
          '';
        };
      }
    );
}

Ein nix develop in diesem Verzeichnis öffnet eine Shell mit Java 21, Gradle, kubectl und allen weiteren definierten Werkzeugen. Die Versionen sind über die flake.lock auf einen exakten nixpkgs-Commit gepinnt. Jeder im Team, der denselben Commit auscheckt, erhält exakt dieselben Werkzeuge in exakt denselben Versionen. Nicht ähnlich -- identisch.

direnv: Automatische Aktivierung

nix develop jedes Mal manuell ausführen, wenn man in ein Projektverzeichnis wechselt, wäre umständlich. Hier kommt direnv ins Spiel. direnv ist ein Shell-Hook, der beim Betreten eines Verzeichnisses automatisch Umgebungsvariablen setzt -- und mit der Erweiterung nix-direnv aktiviert es automatisch die passende Nix-devShell.

Die Einrichtung ist minimal. Eine .envrc-Datei im Projektstamm genügt:

# .envrc
use flake

Zwei Wörter. Wenn wir mit cd in das Projektverzeichnis wechseln, erkennt direnv die .envrc, aktiviert die devShell aus der flake.nix, und alle Werkzeuge sind verfügbar. Beim Verlassen des Verzeichnisses wird die Umgebung wieder aufgeräumt. Kein manuelles Aktivieren, kein Deaktivieren, kein "habe ich die richtige Java-Version aktiv?".

Der erste Aufruf nach einer Änderung an der flake.nix oder flake.lock braucht etwas Zeit, weil Nix die Abhängigkeiten evaluiert und gegebenenfalls herunterlädt. Danach ist die Aktivierung praktisch sofort -- nix-direnv cached das Ergebnis, und der Wechsel ins Verzeichnis fügt den Tools nur den PATH hinzu.

Vergleich: Nix devShell vs. Docker Compose für Development

Aspekt Docker Compose Nix devShell + direnv
Daemon erforderlich Ja (Docker Daemon) Nein
Ressourcenverbrauch Hoch (Linux-VM auf macOS) Minimal (native Prozesse)
Aktivierungszeit 30-60 Sekunden < 1 Sekunde (nach Cache)
Versionspinning Per Image-Tag, manuell Automatisch via flake.lock
Reproduzierbarkeit Abhängig von Image-Tags Exakt via nixpkgs-Commit
Projekt-Isolation Per Container Per devShell / direnv
Plattformunabhängig Ja Linux, macOS (nativ)
Lernkurve Niedrig Mittel bis hoch

Die Tabelle zeigt: Für Entwicklungswerkzeuge -- Compiler, Build-Tools, CLI-Utilities -- ist Nix leichter, schneller und reproduzierbarer. Docker Compose hat seine Stärke weiterhin bei Services, die als Prozess laufen müssen: Datenbanken, Message Broker, Caches. In der Praxis kombinieren wir beides: Nix für die Werkzeuge, Docker Compose für externe Services, die einen laufenden Server-Prozess brauchen.

Mehrere Projekte, verschiedene Versionen

Der größte Alltagsvorteil zeigt sich, wenn man an mehreren Projekten gleichzeitig arbeitet. Projekt A braucht Java 17 und Gradle 7, Projekt B braucht Java 21 und Gradle 8, ein Nebenprojekt braucht Node.js 20. Ohne Nix bedeutet das sdkman, nvm, asdf oder ähnliche Versionmanager -- die funktionieren, aber jeder hat seine eigenen Macken und keiner davon ist wirklich deklarativ.

Mit Nix hat jedes Projekt seine eigene flake.nix, seine eigene .envrc, und seine eigenen gepinnten Versionen. Ein cd genügt, und die richtige Umgebung ist aktiv. Kein sdk use java 17.0.9-tem, kein nvm use 20. direnv macht das automatisch, und zwar zuverlässig.

Was wir damit nicht machen

Ein wichtiger Punkt, den wir explizit machen wollen: Diese Nix-Entwicklungsumgebungen ersetzen nicht unsere Container-basierte Deployment-Pipeline. Unsere Anwendungen laufen in Produktion als Docker-Container auf Kubernetes. Jib baut die Container-Images, Helm deployt sie, K3s führt sie aus. Daran ändert Nix nichts.

Nix ersetzt auch nicht Docker Compose für laufende Services. Wenn wir lokal eine PostgreSQL-Datenbank oder einen Redis brauchen, starten wir die weiterhin per Docker Compose. Nix stellt uns den psql-Client bereit, aber nicht den Server-Prozess.

Die Trennlinie ist klar: Nix verwaltet Entwicklungswerkzeuge -- alles, was in den PATH gehört. Docker verwaltet laufende Services und Produktions-Deployments. Das sind komplementäre Werkzeuge, keine Konkurrenten.

Einstieg ins Team

Die Einführung im Team war einfacher als erwartet. Die Voraussetzungen sind überschaubar: Nix muss installiert sein (ein Einzeiler), Flakes müssen aktiviert sein, und direnv muss in der Shell eingerichtet sein. Danach funktioniert es für jeden gleich -- das ist ja der Sinn der Sache.

Der größte Widerstand kam überraschenderweise nicht von der Nix-Syntax, sondern von der Umstellung des Workflows. Wer jahrelang Java per sdkman verwaltet hat, muss sich umgewöhnen, dass die Java-Version jetzt im Repository steht und nicht mehr auf der eigenen Maschine. Aber genau das ist der Punkt: Die Entwicklungsumgebung ist Teil des Projekts, nicht eine individuelle Konfiguration auf dem Laptop des Entwicklers.

Fazit

Nix Flakes und direnv haben unsere lokale Entwicklungserfahrung deutlich verbessert. Schnellere Aktivierung, geringerer Ressourcenverbrauch, exakte Reproduzierbarkeit der Werkzeugversionen -- das sind keine abstrakten Vorteile, sondern spürbare Verbesserungen im Alltag.

Wer bereits Erfahrung mit Nix hat -- sei es durch NixOS auf dem Server oder durch andere Berührungspunkte -- sollte devShells und direnv ausprobieren. Die Einrichtung dauert eine halbe Stunde, und der Mehrwert zeigt sich beim ersten Projektwechsel. Die Lernkurve für Nix-Neulinge ist steiler, aber die Investition lohnt sich -- gerade in Teams, wo "bei mir funktioniert es" der häufigste Debug-Satz ist.

Docker bleibt ein unverzichtbares Werkzeug in unserem Stack -- für Produktion, für Container-Images, für externe Services. Aber als Grundlage für lokale Entwicklungsumgebungen hat Nix es bei uns abgelöst. Leichter, schneller, reproduzierbarer.

Teilen

Patrick Hütter

Geschrieben von

Patrick Hütter

Gründer & Software-Architekt

Softwarearchitekt, Ingenieur und Unternehmer. Patrick baut seit über einem Jahrzehnt Produkte und Plattformen — von Enterprise-Backends über Cloud-Native-Infrastruktur bis hin zu KI-gestützten Anwendungen. Als Gründer von encircle360 verbindet er technische Tiefe mit unternehmerischem Denken und treibt Open-Source-Projekte voran, die echten Impact haben.