Django auf Ubuntu zu deployen ist dokumentiert, gut verstanden und in tausend Blog-Artikeln beschrieben. Django auf NixOS zu deployen ist das nicht – und genau deshalb schreibe ich diesen Artikel. Meine eigene Website läuft seit über einem Jahr produktiv auf diesem Setup, ohne einen einzigen ungeplanten Ausfall.

Der Stack ist bewusst unspektakulär: Django 6, Gunicorn, WhiteNoise, SQLite. Kein Docker, kein Kubernetes, kein Redis. Das klingt nach Kompromiss – ist aber in der Praxis für die meisten Webprojekte die richtige Wahl.

Warum kein Docker?

Auf NixOS ist Docker nicht notwendig. Die Isolation, die Docker auf anderen Systemen bietet – eigene Abhängigkeiten, reproduzierbarer Build, saubere Trennung vom Host – bekommt man auf NixOS durch den Nix-Paketmanager selbst. Jedes Paket hat seinen eigenen Store-Path, keine Versionskonflikte zwischen Projekten, vollständige Reproduzierbarkeit.

Statt eines Dockerfiles schreibt man einen systemd-Service in die configuration.nix.

Der systemd-Service

Der Service startet Gunicorn als Unix-Socket-basierter Prozess unter einem dedizierten System-User:

systemd.services.meinprojekt = {
  description = "Mein Django Projekt (Gunicorn)";
  after = [ "network.target" ];
  wantedBy = [ "multi-user.target" ];
  serviceConfig = {
    User = "meinprojekt";
    Group = "meinprojekt";
    WorkingDirectory = "/var/www/meinprojekt";
    EnvironmentFile = "/var/lib/meinprojekt/.env";
    ExecStart = "${venv}/bin/gunicorn meinprojekt.wsgi:application --workers 3 --bind unix:/run/meinprojekt/gunicorn.sock";
    RuntimeDirectory = "meinprojekt";
    Restart = "on-failure";
  };
};

Wichtig: EnvironmentFile lädt die .env-Datei mit Secrets aus einem Verzeichnis außerhalb des Git-Repos. So liegen Datenbankpfad, Static Root und andere sensitive Werte nie im Repository.

Nginx als Reverse Proxy

Nginx leitet Anfragen an den Gunicorn-Socket weiter. Auf NixOS wird Nginx deklarativ konfiguriert:

services.nginx.virtualHosts."example.com" = {
  locations."/" = {
    proxyPass = "http://unix:/run/meinprojekt/gunicorn.sock";
    proxyWebsockets = true;
    extraConfig = ''
      proxy_set_header Host $host;
      proxy_set_header X-Forwarded-Proto $scheme;
    '';
  };
};

WhiteNoise für Static Files

WhiteNoise serviert statische Dateien direkt aus Gunicorn heraus – ohne separaten Nginx-location-Block für /static/. Das vereinfacht die Konfiguration erheblich.

MIDDLEWARE = [
    "whitenoise.middleware.WhiteNoiseMiddleware",
    # ... andere Middleware
]

STATIC_ROOT = Path(os.getenv("DJANGO_STATIC_ROOT", str(BASE_DIR / "staticfiles")))

Ein kritischer Punkt: collectstatic muss mit explizit gesetzter DJANGO_STATIC_ROOT-Umgebungsvariable ausgeführt werden – sonst kopiert es ins Repo-Verzeichnis statt ins State-Verzeichnis. Und danach muss die Eigentümerschaft auf den Service-User gesetzt werden, weil collectstatic oft als root läuft und Gunicorn sonst keine Leserechte hat:

DJANGO_STATIC_ROOT=/var/lib/meinprojekt/staticfiles   venv/bin/python manage.py collectstatic --noinput
chown -R meinprojekt:meinprojekt /var/lib/meinprojekt/staticfiles/

SQLite in Produktion – wirklich?

Ja, wirklich. Für Webprojekte mit moderatem Traffic ist SQLite nicht nur akzeptabel – es ist oft die bessere Wahl. Kein separater Datenbankserver, keine Verbindungsprobleme, Backups sind ein einfaches Kopieren der Datenbankdatei. Django 4.2+ hat zudem WAL-Mode und Connection Pooling für SQLite stark verbessert.

Das State-Verzeichnis-Prinzip

Alles, was sich zur Laufzeit ändert und nicht in Git gehört, liegt in einem dedizierten State-Verzeichnis außerhalb des Repos:

  • Datenbank (.sqlite3)
  • Kompilierte statische Dateien (staticfiles/)
  • Hochgeladene Medien (media/)
  • Python-Virtualenv (venv/)
  • Secrets (.env)

Das Code-Repo könnte jederzeit gelöscht und neu geklont werden, ohne Daten zu verlieren.

Weitere Details zum Infrastruktur-Aufbau finden sich in den Artikeln über NixOS als Serverbetriebssystem und Cloudflare Tunnel.