forked from Fediversity/Fediversity
This setup is greatly inspired by the one used for [0], although with notable modifications, such as: - a SASS preprocessor and CSS compressor - more streamlined NixOS integration tests - cleaned up service configuration - a few notes on how to do things better in the future [0]: https://github.com/Nix-Security-WG/nix-security-tracker/ Apart from cloning the Nix setup, there were additional steps: - Create an empty `src` directory, since the package requires it - In the development shell, run `django-admin startproject panel src` Note that while you can already do ```bash manage migrate manage runserver ``` the NixOS integration tests will fail, since `settings.py` needs careful massaging to expose knobs that can be turned from our systemd wrapper. The required changes are introduced in the next commit to make them observable. Noteworthy related work: - https://github.com/sephii/django.nix Rather mature setup with a clean interface, uses Caddy as reverse proxy. - https://git.dgnum.eu/mdebray/djangonix A work-in-progress attempt to capture more moving parts through the module system, in particular secrets. - https://github.com/DavHau/django-nixos Out of date and somewhat simplistic, but serves as a reasonable example for what can be done I chose the variant I'm intimately familiar with in order to be able to pass on knowledge or help with maintenance. But for the future I strongly recommend picking the good bits from the other implementations that control complexity in static configuration parts through Nix expressions.
199 lines
5.3 KiB
Nix
199 lines
5.3 KiB
Nix
{
|
|
config,
|
|
pkgs,
|
|
lib,
|
|
...
|
|
}:
|
|
let
|
|
inherit (lib)
|
|
concatStringsSep
|
|
mapAttrsToList
|
|
mkDefault
|
|
mkEnableOption
|
|
mkIf
|
|
mkOption
|
|
mkPackageOption
|
|
optionalString
|
|
types
|
|
;
|
|
inherit (pkgs) writeShellApplication;
|
|
|
|
# TODO: configure the name globally for everywhere it's used
|
|
name = "panel";
|
|
|
|
cfg = config.services.${name};
|
|
|
|
database-url = "sqlite:////var/lib/${name}/db.sqlite3";
|
|
|
|
python-environment = pkgs.python3.withPackages (
|
|
ps: with ps; [
|
|
cfg.package
|
|
uvicorn
|
|
]
|
|
);
|
|
|
|
configFile = pkgs.concatText "configuration.py" [
|
|
((pkgs.formats.pythonVars { }).generate "settings.py" cfg.settings)
|
|
(builtins.toFile "extra-settings.py" cfg.extra-settings)
|
|
];
|
|
|
|
manage-service = writeShellApplication {
|
|
name = "manage";
|
|
text = ''exec ${cfg.package}/bin/manage.py "$@"'';
|
|
};
|
|
|
|
manage-admin = writeShellApplication {
|
|
# This allows running the `manage` command in the system environment, e.g. to initialise an admin user
|
|
# Executing
|
|
name = "manage";
|
|
text =
|
|
''
|
|
systemd-run --pty \
|
|
--same-dir \
|
|
--wait \
|
|
--collect \
|
|
--service-type=exec \
|
|
--unit "manage-${name}.service" \
|
|
--property "User=${name}" \
|
|
--property "Group=${name}" \
|
|
--property "Environment=DATABASE_URL=${database-url} USER_SETTINGS_FILE=${configFile}" \
|
|
''
|
|
+ optionalString (credentials != [ ]) (
|
|
(concatStringsSep " \\\n" (map (cred: "--property 'LoadCredential=${cred}'") credentials)) + " \\\n"
|
|
)
|
|
+ ''
|
|
${lib.getExe manage-service} "$@"
|
|
'';
|
|
};
|
|
|
|
credentials = mapAttrsToList (name: secretPath: "${name}:${secretPath}") cfg.secrets;
|
|
in
|
|
# TODO: for a more clever and generic way of running Django services:
|
|
# https://git.dgnum.eu/mdebray/djangonix/
|
|
# unlicensed at the time of writing, but surely worth taking some inspiration from...
|
|
{
|
|
options.services.${name} = {
|
|
enable = mkEnableOption "Service configuration for `${name}`";
|
|
# NOTE: this requires that the package is present in `pkgs`
|
|
package = mkPackageOption pkgs name { };
|
|
production = mkOption {
|
|
type = types.bool;
|
|
default = true;
|
|
};
|
|
restart = mkOption {
|
|
description = "systemd restart behavior";
|
|
type = types.enum [
|
|
"no"
|
|
"on-success"
|
|
"on-failure"
|
|
"on-abnormal"
|
|
"on-abort"
|
|
"always"
|
|
];
|
|
default = "always";
|
|
};
|
|
domain = mkOption { type = types.str; };
|
|
host = mkOption {
|
|
type = types.str;
|
|
default = "127.0.0.1";
|
|
};
|
|
port = mkOption {
|
|
type = types.port;
|
|
default = 8000;
|
|
};
|
|
settings = mkOption {
|
|
type = types.attrsOf types.anything;
|
|
default = {
|
|
STATIC_ROOT = mkDefault "/var/lib/${name}/static";
|
|
DEBUG = mkDefault false;
|
|
ALLOWED_HOSTS = mkDefault [
|
|
cfg.domain
|
|
cfg.host
|
|
"localhost"
|
|
"[::1]"
|
|
];
|
|
CSRF_TRUSTED_ORIGINS = mkDefault [ "https://${cfg.domain}" ];
|
|
COMPRESS_OFFLINE = true;
|
|
LIBSASS_OUTPUT_STYLE = "compressed";
|
|
};
|
|
description = ''
|
|
Django configuration as an attribute set.
|
|
Name-value pairs will be converted to Python variable assignments.
|
|
'';
|
|
};
|
|
extra-settings = mkOption {
|
|
type = types.lines;
|
|
default = "";
|
|
description = ''
|
|
Django configuration written in Python verbatim.
|
|
Contents will be appended to the definitions in `settings`.
|
|
'';
|
|
};
|
|
secrets = mkOption {
|
|
type = types.attrsOf types.path;
|
|
default = { };
|
|
};
|
|
};
|
|
|
|
config = mkIf cfg.enable {
|
|
environment.systemPackages = [ manage-admin ];
|
|
|
|
services = {
|
|
nginx.enable = true;
|
|
nginx.virtualHosts = {
|
|
${cfg.domain} =
|
|
{
|
|
locations = {
|
|
"/".proxyPass = "http://localhost:${toString cfg.port}";
|
|
"/static/".alias = "/var/lib/${name}/static/";
|
|
};
|
|
}
|
|
// lib.optionalAttrs cfg.production {
|
|
enableACME = true;
|
|
forceSSL = true;
|
|
};
|
|
};
|
|
};
|
|
|
|
users.users.${name} = {
|
|
isSystemUser = true;
|
|
group = name;
|
|
};
|
|
|
|
users.groups.${name} = { };
|
|
systemd.services.${name} = {
|
|
description = "${name} ASGI server";
|
|
after = [ "network.target" ];
|
|
wantedBy = [ "multi-user.target" ];
|
|
path = [
|
|
python-environment
|
|
manage-service
|
|
];
|
|
preStart = ''
|
|
# Auto-migrate on first run or if the package has changed
|
|
versionFile="/var/lib/${name}/package-version"
|
|
if [[ $(cat "$versionFile" 2>/dev/null) != ${cfg.package} ]]; then
|
|
manage migrate --no-input
|
|
manage collectstatic --no-input --clear
|
|
manage compress --force
|
|
echo ${cfg.package} > "$versionFile"
|
|
fi
|
|
'';
|
|
script = ''
|
|
uvicorn ${name}.asgi:application --host ${cfg.host} --port ${toString cfg.port}
|
|
'';
|
|
serviceConfig = {
|
|
Restart = "always";
|
|
User = name;
|
|
WorkingDirectory = "/var/lib/${name}";
|
|
StateDirectory = name;
|
|
RuntimeDirectory = name;
|
|
LogsDirectory = name;
|
|
} // lib.optionalAttrs (credentials != [ ]) { LoadCredential = credentials; };
|
|
environment = {
|
|
USER_SETTINGS_FILE = "${configFile}";
|
|
DATABASE_URL = database-url;
|
|
};
|
|
};
|
|
};
|
|
}
|