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.