Django
Building on the previous simple testing example we're building a web application using Django.
This example aims to be a showcase of many different capabilities & possibilites of uv2nix
:
-
Packaging a web application using Django
-
Using Django's staticfiles app
-
Constructing Docker containers
Using
dockerTools
from nixpkgs. -
Creating NixOS modules for
uv2nix
appsWith accompanying NixOS tests.
A real-world deployment should use a reverse proxy, for example
nginx
. Use this documentation as inspiration, not as a best practices guide.
flake.nix
{
description = "Django application using uv2nix";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
pyproject-nix = {
url = "github:pyproject-nix/pyproject.nix";
inputs.nixpkgs.follows = "nixpkgs";
};
uv2nix = {
url = "github:pyproject-nix/uv2nix";
inputs.pyproject-nix.follows = "pyproject-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
pyproject-build-systems = {
url = "github:pyproject-nix/build-system-pkgs";
inputs.pyproject-nix.follows = "pyproject-nix";
inputs.uv2nix.follows = "uv2nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
{
self,
nixpkgs,
uv2nix,
pyproject-nix,
pyproject-build-systems,
...
}:
let
inherit (nixpkgs) lib;
forAllSystems = lib.genAttrs lib.systems.flakeExposed;
asgiApp = "django_webapp.asgi:application";
settingsModules = {
prod = "django_webapp.settings";
};
workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; };
overlay = workspace.mkPyprojectOverlay {
sourcePreference = "wheel";
};
editableOverlay = workspace.mkEditablePyprojectOverlay {
root = "$REPO_ROOT";
};
# Python sets grouped per system
pythonSets = forAllSystems (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
inherit (pkgs) stdenv;
# Base Python package set from pyproject.nix
baseSet = pkgs.callPackage pyproject-nix.build.packages {
python = pkgs.python312;
};
# An overlay of build fixups & test additions
pyprojectOverrides = final: prev: {
# django-webapp is the name of our example package
django-webapp = prev.django-webapp.overrideAttrs (old: {
# Add tests to passthru.tests
#
# These attribute are used in Flake checks.
passthru = old.passthru // {
tests =
(old.tests or { })
// {
# Run mypy checks
mypy =
let
venv = final.mkVirtualEnv "django-webapp-typing-env" {
django-webapp = [ "typing" ];
};
in
stdenv.mkDerivation {
name = "${final.django-webapp.name}-mypy";
inherit (final.django-webapp) src;
nativeBuildInputs = [
venv
];
dontConfigure = true;
dontInstall = true;
buildPhase = ''
mkdir $out
mypy --strict . --junit-xml $out/junit.xml
'';
};
# Run pytest with coverage reports installed into build output
pytest =
let
venv = final.mkVirtualEnv "django-webapp-pytest-env" {
django-webapp = [ "test" ];
};
in
stdenv.mkDerivation {
name = "${final.django-webapp.name}-pytest";
inherit (final.django-webapp) src;
nativeBuildInputs = [
venv
];
dontConfigure = true;
buildPhase = ''
runHook preBuild
pytest --cov tests --cov-report html tests
runHook postBuild
'';
installPhase = ''
runHook preInstall
mv htmlcov $out
runHook postInstall
'';
};
}
// lib.optionalAttrs stdenv.isLinux {
# NixOS module test
nixos =
let
venv = final.mkVirtualEnv "django-webapp-nixos-test-env" {
django-webapp = [ ];
};
in
pkgs.nixosTest {
name = "django-webapp-nixos-test";
nodes.machine =
{ ... }:
{
imports = [
self.nixosModules.django-webapp
];
services.django-webapp = {
enable = true;
inherit venv;
};
system.stateVersion = "24.11";
};
testScript = ''
machine.wait_for_unit("django-webapp.service")
with subtest("Web interface getting ready"):
machine.wait_until_succeeds("curl -fs localhost:8000")
'';
};
};
};
});
};
in
baseSet.overrideScope (
lib.composeManyExtensions [
pyproject-build-systems.overlays.default
overlay
pyprojectOverrides
]
)
);
# Django static roots grouped per system
staticRoots = forAllSystems (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
inherit (pkgs) stdenv;
pythonSet = pythonSets.${system};
venv = pythonSet.mkVirtualEnv "django-webapp-env" workspace.deps.default;
in
stdenv.mkDerivation {
name = "django-webapp-static";
inherit (pythonSet.django-webapp) src;
dontConfigure = true;
dontBuild = true;
nativeBuildInputs = [
venv
];
installPhase = ''
env DJANGO_STATIC_ROOT="$out" python manage.py collectstatic
'';
}
);
in
{
checks = forAllSystems (
system:
let
pythonSet = pythonSets.${system};
in
# Inherit tests from passthru.tests into flake checks
pythonSet.django-webapp.passthru.tests
);
nixosModules = {
django-webapp =
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.django-webapp;
inherit (pkgs) system;
pythonSet = pythonSets.${system};
inherit (lib.options) mkOption;
inherit (lib.modules) mkIf;
in
{
options.services.django-webapp = {
enable = mkOption {
type = lib.types.bool;
default = false;
description = ''
Enable django-webapp
'';
};
settings-module = mkOption {
type = lib.types.string;
default = settingsModules.prod;
description = ''
Django settings module
'';
};
venv = mkOption {
type = lib.types.package;
default = pythonSet.mkVirtualEnv "django-webapp-env" workspace.deps.default;
description = ''
Django-webapp virtual environment package
'';
};
static-root = mkOption {
type = lib.types.package;
default = staticRoots.${system};
description = ''
Django-webapp static root
'';
};
};
config = mkIf cfg.enable {
systemd.services.django-webapp = {
description = "Django Webapp server";
environment.DJANGO_STATIC_ROOT = cfg.static-root;
serviceConfig = {
ExecStart = ''
${cfg.venv}/bin/daphne django_webapp.asgi:application
'';
Restart = "on-failure";
DynamicUser = true;
StateDirectory = "django-webapp";
RuntimeDirectory = "django-webapp";
BindReadOnlyPaths = [
"${
config.environment.etc."ssl/certs/ca-certificates.crt".source
}:/etc/ssl/certs/ca-certificates.crt"
builtins.storeDir
"-/etc/resolv.conf"
"-/etc/nsswitch.conf"
"-/etc/hosts"
"-/etc/localtime"
];
};
wantedBy = [ "multi-user.target" ];
};
};
};
};
packages = forAllSystems (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
pythonSet = pythonSets.${system};
in
lib.optionalAttrs pkgs.stdenv.isLinux {
# Expose Docker container in packages
docker =
let
venv = pythonSet.mkVirtualEnv "django-webapp-env" workspace.deps.default;
in
pkgs.dockerTools.buildLayeredImage {
name = "django-webapp";
contents = [ pkgs.cacert ];
config = {
Cmd = [
"${venv}/bin/daphne"
asgiApp
];
Env = [
"DJANGO_SETTINGS_MODULE=${settingsModules.prod}"
"DJANGO_STATIC_ROOT=${staticRoots.${system}}"
];
};
};
}
);
# Use an editable Python set for development.
devShells = forAllSystems (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
editablePythonSet = pythonSets.${system}.overrideScope (
lib.composeManyExtensions [
editableOverlay
(final: prev: {
django-webapp = prev.django-webapp.overrideAttrs (old: {
src = lib.fileset.toSource {
root = old.src;
fileset = lib.fileset.unions [
(old.src + "/pyproject.toml")
(old.src + "/README.md")
(old.src + "/src/django_webapp/__init__.py")
];
};
nativeBuildInputs =
old.nativeBuildInputs
++ final.resolveBuildSystem {
editables = [ ];
};
});
})
]
);
venv = editablePythonSet.mkVirtualEnv "django-webapp-dev-env" {
django-webapp = [ "dev" ];
};
in
{
default = pkgs.mkShell {
packages = [
venv
pkgs.uv
];
env = {
UV_NO_SYNC = "1";
UV_PYTHON = "${venv}/bin/python";
UV_PYTHON_DOWNLOADS = "never";
};
shellHook = ''
unset PYTHONPATH
export REPO_ROOT=$(git rev-parse --show-toplevel)
'';
};
}
);
};
}
pyproject.toml
[project]
name = "django-webapp"
version = "0.1.0"
description = "A django web application developed & deployed using uv2nix"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"django>=5.1.3",
"daphne>=4.1.2",
]
[dependency-groups]
dev = [
{include-group = "test"},
{include-group = "typing"},
{include-group = "lint"},
]
typing = [
"django-stubs[compatible-mypy]>=5.1.1",
"mypy>=1.13.0",
]
test = [
"pytest-cov>=6.0.0",
"pytest-django>=4.9.0",
"pytest>=8.3.3",
]
lint = [
"ruff>=0.7.2",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.pytest.ini_options]
DJANGO_SETTINGS_MODULE = "django_webapp.settings"
[tool.mypy]
exclude = ["manage.py"]
plugins = ["mypy_django_plugin.main"]
[tool.django-stubs]
django_settings_module = "django_webapp.settings"