Django
Appearance
Django is a high-level Python web framework that encourages rapid development and clean, pragmatic design. It provides built-in components for handling common web-development tasks—such as ORM, templating, authentication, and administration—so developers can focus on writing reusable, maintainable applications.
Development shell
This example flake.nix
can be used for Django web app development on NixOS
{
inputs = {
nixpkgs.url = "nixpkgs/nixos-25.05";
# Required for multi platform support
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; };
start =
pkgs.writeShellScriptBin "start" ''
set -e
${pkgs.python3}/bin/python manage.py makemigrations
${pkgs.python3}/bin/python manage.py migrate
${pkgs.python3}/bin/python manage.py runserver
'';
in
{
devShell = pkgs.mkShell {
packages = with pkgs; with python3Packages; [
python
django
requests
beautifulsoup4
];
};
packages = { inherit start; };
defaultPackage = start;
});
}
Adapt the dependencies in the devShell packages part according to your project needs. Run the web app with
nix develop
nix run
Packaging
First we create the package derivation for the Django web app, in this example for Froide-Govplan
{
lib,
python3Packages,
fetchFromGitHub,
makeBinaryWrapper,
froide-govplan,
gettext,
}:
let
# Use Django 5 instead of 4
python = python3Packages.python.override {
packageOverrides = self: super: {
django = super.django_5;
};
};
in
python.pkgs.buildPythonApplication rec {
pname = "froide-govplan";
version = "0-unstable-2025-06-25";
pyproject = true;
src = fetchFromGitHub {
owner = "okfde";
repo = "froide-govplan";
rev = "9c325e70a84f26fea37b5a34f24d19fd82ea62ff";
hash = "sha256-OD4vvKt0FLuiAVGwpspWLB2ZuM1UJkZdv2YcbKKYk9A=";
};
build-system = [ python.pkgs.setuptools ];
nativeBuildInputs = [
gettext
makeBinaryWrapper
];
dependencies = with python.pkgs; [
django
[...]
];
preBuild = "${python.interpreter} -m django compilemessages";
postInstall = ''
cp manage.py $out/${python.sitePackages}/froide_govplan/
cp -r project $out/${python.sitePackages}/froide_govplan/
cp -r froide_govplan/locale $out/${python.sitePackages}/froide_govplan/
makeWrapper $out/${python.sitePackages}/froide_govplan/manage.py $out/bin/froide-govplan \
--prefix PYTHONPATH : ${passthru.pythonPath}:$out/${python.sitePackages}
'';
# Make used Python version and Python path available to the module
passthru = {
inherit python;
pythonPath = "${python.pkgs.makePythonPath dependencies}";
};
meta = {
[...]
mainProgram = "froide-govplan";
};
};
An example module derivation would look like this
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.froide-govplan;
pythonFmt = pkgs.formats.pythonVars { };
settingsFile = pythonFmt.generate "extra_settings.py" cfg.settings;
# For this project we supply the extra settings as a Python file
# to a specific path
pkg = cfg.package.overridePythonAttrs (old: {
postInstall = old.postInstall + ''
ln -s ${settingsFile} $out/${pkg.python.sitePackages}/froide_govplan/project/extra_settings.py
'';
});
# Make a wrapper for the main binary (manage.py) to run it
# for the specific user
froide-govplan = pkgs.writeShellApplication {
name = "froide-govplan";
runtimeInputs = [ pkgs.coreutils ];
text = ''
SUDO="exec"
if [[ "$USER" != govplan ]]; then
SUDO="exec /run/wrappers/bin/sudo -u govplan"
fi
$SUDO env ${lib.getExe pkg} "$@"
'';
};
# Service hardening
defaultServiceConfig = {
# Secure the services
ReadWritePaths = [ cfg.dataDir ];
CacheDirectory = "froide-govplan";
CapabilityBoundingSet = "";
# ProtectClock adds DeviceAllow=char-rtc r
DeviceAllow = "";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
ProtectHome = true;
ProtectHostname = true;
ProtectSystem = "strict";
ProtectControlGroups = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProcSubset = "pid";
RestrictAddressFamilies = [
"AF_UNIX"
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged @setuid @keyring"
];
UMask = "0066";
};
in
{
options.services.froide-govplan = {
enable = lib.mkEnableOption "Gouvernment planer web app Govplan";
package = lib.mkPackageOption pkgs "froide-govplan" { };
hostName = lib.mkOption {
type = lib.types.str;
default = "localhost";
description = "FQDN for the froide-govplan instance.";
};
dataDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/froide-govplan";
description = "Directory to store the Froide-Govplan server data.";
};
secretKeyFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Path to a file containing the secret key.
'';
};
settings = lib.mkOption {
description = ''
Configuration options to set in `extra_settings.py`.
'';
default = { };
type = lib.types.submodule {
freeformType = pythonFmt.type;
options = {
ALLOWED_HOSTS = lib.mkOption {
type = with lib.types; listOf str;
default = [ "*" ];
description = ''
A list of valid fully-qualified domain names (FQDNs) and/or IP
addresses that can be used to reach the Froide-Govplan service.
'';
};
};
};
};
};
config = lib.mkIf cfg.enable {
services.froide-govplan = {
settings = {
STATIC_ROOT = "${cfg.dataDir}/static";
DEBUG = false;
DATABASES.default = {
ENGINE = "django.contrib.gis.db.backends.postgis";
NAME = "govplan";
USER = "govplan";
HOST = "/run/postgresql";
};
};
};
services.postgresql = {
enable = true;
ensureDatabases = [ "govplan" ];
ensureUsers = [
{
name = "govplan";
ensureDBOwnership = true;
}
];
};
services.nginx = {
enable = lib.mkDefault true;
virtualHosts."${cfg.hostName}".locations = {
"/".extraConfig = "proxy_pass http://unix:/run/froide-govplan/froide-govplan.socket;";
"/static/".alias = "${cfg.dataDir}/static/";
};
proxyTimeout = lib.mkDefault "120s";
};
systemd = {
services = {
froide-govplan = {
description = "Gouvernment planer Govplan";
serviceConfig = defaultServiceConfig // {
WorkingDirectory = cfg.dataDir;
StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/froide-govplan") "froide-govplan";
User = "govplan";
Group = "govplan";
TimeoutStartSec = "5m";
};
after = [
"postgresql.target"
"network.target"
"systemd-tmpfiles-setup.service"
];
wantedBy = [ "multi-user.target" ];
environment = {
PYTHONPATH = "${pkg.pythonPath}:${pkg}/${pkg.python.sitePackages}";
}
// lib.optionalAttrs (cfg.secretKeyFile != null) {
SECRET_KEY_FILE = cfg.secretKeyFile;
};
preStart = ''
# Auto-migrate on first run or if the package has changed
versionFile="${cfg.dataDir}/src-version"
version=$(cat "$versionFile" 2>/dev/null || echo 0)
if [[ $version != ${pkg.version} ]]; then
${lib.getExe pkg} migrate --no-input
${lib.getExe pkg} collectstatic --no-input --clear
echo ${pkg.version} > "$versionFile"
fi
'';
script = ''
${pkg.python.pkgs.uvicorn}/bin/uvicorn --uds /run/froide-govplan/froide-govplan.socket \
--app-dir ${pkg}/${pkg.python.sitePackages}/froide_govplan \
project.asgi:application
'';
};
};
};
systemd.tmpfiles.rules = [ "d /run/froide-govplan - govplan govplan - -" ];
environment.systemPackages = [ froide-govplan ];
users.users.govplan = {
home = "${cfg.dataDir}";
isSystemUser = true;
group = "govplan";
};
users.groups.govplan = { };
};
meta.maintainers = with lib.maintainers; [ onny ];
}