Initial Bitpoll Nix package and service

This commit is contained in:
Philip Henning 2025-07-03 17:35:13 +02:00
commit 0b3e086c03
5 changed files with 898 additions and 0 deletions

349
module.nix Normal file
View file

@ -0,0 +1,349 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.bitpoll;
# Persistent data directory
dataDir = "/var/lib/bitpoll";
# Generate Django settings file
settingsFile = pkgs.writeText "bitpoll-settings.py" ''
# Bitpoll NixOS Configuration
import os
from bitpoll.settings.production import *
# Security settings
SECRET_KEY = '${cfg.secretKey}'
FIELD_ENCRYPTION_KEY = '${cfg.encryptionKey}'
DEBUG = ${boolToString cfg.debug}
ALLOWED_HOSTS = ${builtins.toJSON cfg.allowedHosts}
# Localization
LANGUAGE_CODE = '${cfg.language}'
TIME_ZONE = '${cfg.timezone}'
# Database configuration
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': '${cfg.database.name}',
'USER': '${cfg.database.user}',
'PASSWORD': '${cfg.database.password}',
'HOST': '${cfg.database.host}',
'PORT': '${toString cfg.database.port}',
}
}
# File storage paths
MEDIA_ROOT = '${dataDir}/media'
STATIC_ROOT = '${dataDir}/static'
# Additional settings
${concatStringsSep "\n" (mapAttrsToList (k: v: "${k} = ${builtins.toJSON v}") cfg.extraSettings)}
'';
# Generate uWSGI configuration
uwsgiConfig = pkgs.writeText "uwsgi.ini" ''
[uwsgi]
procname-master = uwsgi bitpoll
master = true
socket = ${cfg.listenAddress}:${toString cfg.port}
${optionalString (cfg.httpPort != null) "http-socket = ${cfg.listenAddress}:${toString cfg.httpPort}"}
plugins = python3
chdir = ${dataDir}
virtualenv = ${cfg.package}
pythonpath = ${cfg.package}/lib/python*/site-packages
module = bitpoll.wsgi:application
env = DJANGO_SETTINGS_MODULE=bitpoll.settings
env = BITPOLL_SETTINGS_FILE=${settingsFile}
env = LANG=C.UTF-8
env = LC_ALL=C.UTF-8
# Process management
uid = ${cfg.user}
gid = ${cfg.group}
umask = 027
processes = ${toString cfg.processes}
threads = ${toString cfg.threads}
cheaper = ${toString cfg.cheaperProcesses}
# Logging
disable-logging = ${boolToString cfg.disableLogging}
# Static files
static-map = /static=${dataDir}/static
# Additional uWSGI options
${cfg.extraUwsgiConfig}
'';
in {
options.services.bitpoll = {
enable = mkEnableOption "Bitpoll polling application";
package = mkOption {
type = types.package;
default = pkgs.bitpoll or (pkgs.callPackage ./package.nix { });
description = "The Bitpoll package to use";
};
user = mkOption {
type = types.str;
default = "bitpoll";
description = "User account under which Bitpoll runs";
};
group = mkOption {
type = types.str;
default = "bitpoll";
description = "Group under which Bitpoll runs";
};
listenAddress = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Address to listen on";
};
port = mkOption {
type = types.port;
default = 3008;
description = "Port for uWSGI socket";
};
httpPort = mkOption {
type = types.nullOr types.port;
default = 3009;
description = "Port for HTTP socket (null to disable)";
};
# Django settings
secretKey = mkOption {
type = types.str;
description = "Django secret key";
example = "your-secret-key-here";
};
encryptionKey = mkOption {
type = types.str;
description = "Field encryption key";
example = "BnEAJ5eEXb4HfYbaCPuW5RKQSoO02Uhz1RH93eQz0GM=";
};
debug = mkOption {
type = types.bool;
default = false;
description = "Enable Django debug mode";
};
allowedHosts = mkOption {
type = types.listOf types.str;
default = [ "*" ];
description = "List of allowed hosts";
};
language = mkOption {
type = types.str;
default = "en-us";
description = "Language code";
};
timezone = mkOption {
type = types.str;
default = "Europe/Berlin";
description = "Time zone";
};
# Database settings
database = {
name = mkOption {
type = types.str;
default = "bitpoll";
description = "Database name";
};
user = mkOption {
type = types.str;
default = "bitpoll";
description = "Database user";
};
password = mkOption {
type = types.str;
default = "";
description = "Database password";
};
host = mkOption {
type = types.str;
default = "localhost";
description = "Database host";
};
port = mkOption {
type = types.port;
default = 5432;
description = "Database port";
};
};
# uWSGI settings
processes = mkOption {
type = types.int;
default = 8;
description = "Number of uWSGI processes";
};
threads = mkOption {
type = types.int;
default = 4;
description = "Number of threads per process";
};
cheaperProcesses = mkOption {
type = types.int;
default = 2;
description = "Minimum number of processes";
};
disableLogging = mkOption {
type = types.bool;
default = true;
description = "Disable uWSGI request logging";
};
extraUwsgiConfig = mkOption {
type = types.lines;
default = "";
description = "Additional uWSGI configuration";
};
extraSettings = mkOption {
type = types.attrsOf types.anything;
default = { };
description = "Additional Django settings";
};
# PostgreSQL integration
enablePostgreSQL = mkOption {
type = types.bool;
default = true;
description = "Enable and configure PostgreSQL";
};
};
config = mkIf cfg.enable {
# PostgreSQL setup
services.postgresql = mkIf cfg.enablePostgreSQL {
enable = true;
ensureDatabases = [ cfg.database.name ];
ensureUsers = [{
name = cfg.database.user;
ensureDBOwnership = true;
}];
};
# Create user and group
users.users.${cfg.user} = {
isSystemUser = true;
group = cfg.group;
home = dataDir;
createHome = true;
};
users.groups.${cfg.group} = { };
# Create data directories
systemd.tmpfiles.rules = [
"d ${dataDir} 0750 ${cfg.user} ${cfg.group} -"
"d ${dataDir}/media 0750 ${cfg.user} ${cfg.group} -"
"d ${dataDir}/static 0750 ${cfg.user} ${cfg.group} -"
];
# Bitpoll service
systemd.services.bitpoll = {
description = "Bitpoll polling application";
after = [ "network.target" ] ++ optional cfg.enablePostgreSQL "postgresql.service";
wants = optional cfg.enablePostgreSQL "postgresql.service";
wantedBy = [ "multi-user.target" ];
environment = {
DJANGO_SETTINGS_MODULE = "bitpoll.settings";
BITPOLL_SETTINGS_FILE = "${settingsFile}";
PYTHONPATH = "${cfg.package}/lib/python*/site-packages";
};
serviceConfig = {
Type = "notify";
User = cfg.user;
Group = cfg.group;
WorkingDirectory = dataDir;
ExecStart = "${pkgs.uwsgi}/bin/uwsgi --ini ${uwsgiConfig}";
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
KillMode = "mixed";
KillSignal = "SIGINT";
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = true;
ReadWritePaths = [ dataDir ];
NoNewPrivileges = true;
# Security hardening
CapabilityBoundingSet = "";
DeviceAllow = "";
LockPersonality = true;
MemoryDenyWriteExecute = true;
PrivateDevices = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProcSubset = "pid";
RemoveIPC = true;
RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [ "@system-service" "~@privileged @resources" ];
UMask = "0027";
};
# Pre-start script for migrations and static files
preStart = ''
# Wait for database to be ready
${optionalString cfg.enablePostgreSQL ''
while ! ${pkgs.postgresql}/bin/pg_isready -h ${cfg.database.host} -p ${toString cfg.database.port} -U ${cfg.database.user} -d ${cfg.database.name}; do
echo "Waiting for PostgreSQL..."
sleep 2
done
''}
# Run migrations
cd ${dataDir}
export DJANGO_SETTINGS_MODULE=bitpoll.settings
export BITPOLL_SETTINGS_FILE=${settingsFile}
export PYTHONPATH=${cfg.package}/lib/python*/site-packages
${cfg.package}/bin/python ${cfg.package}/manage.py migrate --noinput
${cfg.package}/bin/python ${cfg.package}/manage.py collectstatic --noinput --clear
# Set proper permissions
chown -R ${cfg.user}:${cfg.group} ${dataDir}
chmod -R u=rwX,g=rX,o= ${dataDir}
'';
};
# Open firewall ports if needed
networking.firewall.allowedTCPPorts = mkIf (cfg.httpPort != null) [ cfg.httpPort ];
};
}