feat(core): initialize NaxOS declarative NixOS distribution, modules, and ISO installer
Test NaxOS Module Configurations / test-modules (push) Failing after 6m10s

This commit is contained in:
Lukas Holzner
2026-09-03 23:53:18 +02:00
parent 88a638955b
commit 5da58ae6d7
28 changed files with 2247 additions and 2 deletions
+83
View File
@@ -0,0 +1,83 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.naxos.api;
in {
options.services.naxos.api = {
enable = mkEnableOption "NaxOS Management Daemon & REST/WebSocket API Service";
port = mkOption {
type = types.port;
default = 8088;
description = "Internal daemon listen port.";
};
host = mkOption {
type = types.str;
default = "127.0.0.1";
description = "Internal daemon bind host.";
};
dataDir = mkOption {
type = types.str;
default = "/var/lib/naxos";
description = "Persistent state directory for NaxOS daemon.";
};
configRepoDir = mkOption {
type = types.str;
default = "/etc/naxos/repo";
description = "Directory of the local GitOps configuration repository.";
};
};
config = mkIf cfg.enable {
systemd.services.naxos-api = {
description = "NaxOS Declarative Management Daemon";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" "zfs.target" ];
path = with pkgs; [
zfs
git
nix
systemd
smartmontools
util-linux
shadow
samba
curl
bash
];
environment = {
NODE_ENV = "production";
PORT = toString cfg.port;
HOST = cfg.host;
NAXOS_DATA_DIR = cfg.dataDir;
NAXOS_CONFIG_REPO = cfg.configRepoDir;
SYSTEM_PROFILE = "/nix/var/nix/profiles/system";
};
serviceConfig = {
Type = "simple";
ExecStart = "${pkgs.nodejs}/bin/node /opt/naxos/api/dist/server.js";
WorkingDirectory = cfg.dataDir;
Restart = "always";
RestartSec = "3s";
StateDirectory = "naxos";
RuntimeDirectory = "naxos";
# Sandboxing / Security hardening
ProtectSystem = "strict";
ProtectHome = "read-only";
ReadWritePaths = [ cfg.dataDir cfg.configRepoDir "/var/log" "/etc/naxos" "/nix/var/nix/profiles" ];
AmbientCapabilities = [ "CAP_SYS_ADMIN" ]; # For ZFS operations & systemd manipulation
};
};
systemd.tmpfiles.rules = [
"d ${cfg.dataDir} 0750 root root -"
"d ${cfg.configRepoDir} 0750 root root -"
"d /etc/naxos 0755 root root -"
];
};
}
+111
View File
@@ -0,0 +1,111 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.naxos.gitops;
# Safe switch script with canary validation and automatic rollback
naxosRebuildWrapper = pkgs.writeShellScriptBin "naxos-rebuild-safe" ''
set -euo pipefail
CONFIG_DIR="''${CONFIG_DIR:-/etc/naxos/repo}"
LOG_FILE="/var/log/naxos-rebuild.log"
echo "[$(date -Iseconds)] Starting NaxOS declarative rebuild..." | tee -a "$LOG_FILE"
cd "$CONFIG_DIR"
# Pre-flight check with nix flake check / nixos-rebuild dry-build
echo "Running dry build validation..."
if ! nixos-rebuild build --flake .#naxos 2>&1 | tee -a "$LOG_FILE"; then
echo "Dry-build failed! Configuration aborted without changing running system." | tee -a "$LOG_FILE"
exit 1
fi
# Record current generation
PREV_GEN=$(readlink -f /nix/var/nix/profiles/system)
echo "Applying system switch..."
if nixos-rebuild switch --flake .#naxos 2>&1 | tee -a "$LOG_FILE"; then
echo "System switch succeeded." | tee -a "$LOG_FILE"
# Canary health check: verify management daemon and storage pools are responsive
if systemctl is-active --quiet naxos-api.service; then
echo "Canary verification passed: NaxOS daemon active." | tee -a "$LOG_FILE"
exit 0
else
echo "WARNING: NaxOS daemon failed canary test! Initiating safe automatic rollback..." | tee -a "$LOG_FILE"
"$PREV_GEN/bin/switch-to-configuration" switch
exit 2
fi
else
echo "Switch command failed! Rolling back to $PREV_GEN..." | tee -a "$LOG_FILE"
"$PREV_GEN/bin/switch-to-configuration" switch
exit 3
fi
'';
# Automated remote push/pull service
naxosGitSync = pkgs.writeShellScriptBin "naxos-git-sync" ''
set -euo pipefail
REPO_DIR="/etc/naxos/repo"
if [ ! -d "$REPO_DIR/.git" ]; then
echo "Git repository not initialized in $REPO_DIR."
exit 0
fi
cd "$REPO_DIR"
if [ -n "''${NAXOS_REMOTE_URL:-}" ]; then
echo "Synchronizing with remote GitOps repository..."
git fetch origin main || true
git push origin main || true
fi
'';
in {
options.services.naxos.gitops = {
enable = mkEnableOption "NaxOS GitOps Configuration Synchronization Engine";
remoteUrl = mkOption {
type = types.nullOr types.str;
default = null;
example = "ssh://git@git.lholz.de:2222/naxos/naxos-config.git";
description = "Remote Git repository URL for automated backup, auditing, and disaster recovery.";
};
branch = mkOption {
type = types.str;
default = "main";
description = "GitOps target branch.";
};
tokenFile = mkOption {
type = types.nullOr types.str;
default = null;
description = "Path to Gitea/Git access token or SSH private key.";
};
autoPushOnCommit = mkOption {
type = types.bool;
default = true;
description = "Automatically push to remote repository whenever web dashboard commits a change.";
};
};
config = mkIf cfg.enable {
environment.systemPackages = with pkgs; [
git
naxosRebuildWrapper
naxosGitSync
];
# Create config repo directory structure
systemd.tmpfiles.rules = [
"d /etc/naxos 0755 root root -"
"d /etc/naxos/repo 0750 root root -"
"d /var/log 0755 root root -"
];
};
}
+77
View File
@@ -0,0 +1,77 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.naxos.selfUpdate;
updateScript = pkgs.writeShellScriptBin "naxos-self-update" ''
set -euo pipefail
CONFIG_DIR="''${CONFIG_DIR:-/etc/naxos/repo}"
LOG_FILE="/var/log/naxos-update.log"
echo "[$(date -Iseconds)] Checking for NaxOS updates..." | tee -a "$LOG_FILE"
if [ ! -d "$CONFIG_DIR/.git" ]; then
echo "Configuration directory is not a git repository. Skipping." | tee -a "$LOG_FILE"
exit 0
fi
cd "$CONFIG_DIR"
git fetch origin main
LOCAL_HASH=$(git rev-parse HEAD)
REMOTE_HASH=$(git rev-parse origin/main)
if [ "$LOCAL_HASH" = "$REMOTE_HASH" ]; then
echo "System is already up to date ($LOCAL_HASH)." | tee -a "$LOG_FILE"
exit 0
fi
echo "New updates detected ($LOCAL_HASH -> $REMOTE_HASH). Updating flake inputs and pulling..." | tee -a "$LOG_FILE"
git merge origin/main --ff-only
echo "Rebuilding and applying configuration..." | tee -a "$LOG_FILE"
naxos-rebuild-safe
'';
in {
options.services.naxos.selfUpdate = {
enable = mkEnableOption "NaxOS Automated Background Self-Update Service";
schedule = mkOption {
type = types.str;
default = "*-*-* 04:00:00"; # Daily at 4:00 AM
description = "Systemd calendar expression for scheduled update checks.";
};
channel = mkOption {
type = types.enum [ "stable" "beta" "nightly" ];
default = "stable";
description = "Update release channel.";
};
};
config = mkIf cfg.enable {
environment.systemPackages = [ updateScript ];
systemd.services.naxos-self-update = {
description = "NaxOS Self-Update Check & Apply";
path = [ pkgs.git pkgs.nix pkgs.systemd pkgs.bash ];
serviceConfig = {
Type = "oneshot";
ExecStart = "${updateScript}/bin/naxos-self-update";
};
};
systemd.timers.naxos-self-update = {
description = "NaxOS Scheduled Self-Update Timer";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = cfg.schedule;
Persistent = true;
RandomizedDelaySec = "1800"; # 30 min random jitter
};
};
};
}
+96
View File
@@ -0,0 +1,96 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.naxos.core;
in {
options.services.naxos.core = {
enable = mkEnableOption "NaxOS Core Appliance Base";
hostName = mkOption {
type = types.str;
default = "naxos";
description = "Appliance hostname.";
};
hostId = mkOption {
type = types.str;
default = "8425f3a1";
description = "32-bit Host ID required for OpenZFS safety locking.";
};
timeZone = mkOption {
type = types.str;
default = "Europe/Berlin";
description = "System timezone.";
};
};
config = mkIf cfg.enable {
networking.hostName = cfg.hostName;
networking.hostId = cfg.hostId;
time.timeZone = cfg.timeZone;
# Flakes and Nix CLI enablement
nix.settings = {
experimental-features = [ "nix-command" "flakes" ];
auto-optimise-store = true;
};
# High-Performance Storage & Network Kernel Tuning
boot.kernel.sysctl = {
# BBR Congestion Control
"net.core.default_qdisc" = "fq";
"net.ipv4.tcp_congestion_control" = "bbr";
# High-bandwidth 10G/25G network buffer tuning
"net.core.rmem_max" = 67108864;
"net.core.wmem_max" = 67108864;
"net.ipv4.tcp_rmem" = "4096 87380 33554432";
"net.ipv4.tcp_wmem" = "4096 65536 33554432";
"net.core.netdev_max_backlog" = 10000;
# Storage & VM writeback tuning for ZFS
"vm.swappiness" = 10;
"vm.dirty_background_ratio" = 5;
"vm.dirty_ratio" = 10;
# File handles limit
"fs.file-max" = 2097152;
};
# Core system tools
environment.systemPackages = with pkgs; [
curl
wget
git
htop
btop
tmux
jq
pciutils
usbutils
ethtool
iperf3
rsync
];
# Security & Firewall defaults
networking.firewall = {
enable = true;
allowPing = true;
allowedTCPPorts = [ 22 80 443 ];
};
# SSH Server with modern secure defaults
services.openssh = {
enable = true;
settings = {
PermitRootLogin = "prohibit-password";
PasswordAuthentication = false;
KbdInteractiveAuthentication = false;
};
};
};
}
+80
View File
@@ -0,0 +1,80 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.naxos.users;
in {
options.services.naxos.users = {
enable = mkEnableOption "NaxOS Declarative User Management";
adminUser = mkOption {
type = types.str;
default = "admin";
description = "Primary administrator account name.";
};
adminSshKeys = mkOption {
type = types.listOf types.str;
default = [];
description = "Public SSH keys for the administrator.";
};
users = mkOption {
type = types.attrsOf (types.submodule {
options = {
description = mkOption {
type = types.str;
default = "";
description = "User full name or comment.";
};
isAdmin = mkOption {
type = types.bool;
default = false;
description = "Whether the user has sudo/wheel privileges.";
};
sshKeys = mkOption {
type = types.listOf types.str;
default = [];
description = "Public SSH authorized keys.";
};
extraGroups = mkOption {
type = types.listOf types.str;
default = [];
description = "Additional Linux groups.";
};
smbAccess = mkOption {
type = types.bool;
default = true;
description = "Whether the user can access SMB shares.";
};
};
});
default = {};
description = "Appliance user accounts.";
};
};
config = mkIf cfg.enable {
users.mutableUsers = true;
# Admin user creation
users.users.${cfg.adminUser} = {
isNormalUser = true;
description = "NaxOS Primary Administrator";
extraGroups = [ "wheel" "docker" "video" "render" "users" ];
openssh.authorizedKeys.keys = cfg.adminSshKeys;
shell = pkgs.bashInteractive;
};
# Additional users
users.users = mapAttrs (name: ucfg: {
isNormalUser = true;
description = ucfg.description;
extraGroups = (if ucfg.isAdmin then [ "wheel" ] else []) ++ ucfg.extraGroups ++ [ "users" ];
openssh.authorizedKeys.keys = ucfg.sshKeys;
}) cfg.users;
security.sudo.wheelNeedsPassword = false;
};
}
+19
View File
@@ -0,0 +1,19 @@
{ ... }:
{
imports = [
./core/system.nix
./core/users.nix
./core/gitops.nix
./core/self-update.nix
./storage/zfs.nix
./shares/samba.nix
./shares/nfs.nix
./services/app-engine.nix
./services/apps
./monitoring/prometheus.nix
./monitoring/perses.nix
./api/daemon.nix
./ui/service.nix
];
}
+186
View File
@@ -0,0 +1,186 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.naxos.perses;
# Declarative Perses Prometheus Datasource Manifest
datasourceManifest = pkgs.writeText "perses-prom-datasource.json" (builtins.toJSON {
kind = "Datasource";
metadata = {
name = "PrometheusDemo";
project = "naxos";
};
spec = {
default = true;
plugin = {
kind = "PrometheusDatasource";
spec = {
directUrl = "http://127.0.0.1:9090";
};
};
};
});
# Declarative Perses ZFS & System Dashboard Manifest
zfsDashboardManifest = pkgs.writeText "perses-zfs-dashboard.json" (builtins.toJSON {
kind = "Dashboard";
metadata = {
name = "zfs-storage-health";
project = "naxos";
};
spec = {
duration = "1h";
refreshInterval = "10s";
display = {
name = "ZFS Storage & System Health";
description = "Native NaxOS analytics powered by Perses";
};
variables = [];
panels = {
cpuUsage = {
kind = "Panel";
spec = {
display = { name = "CPU Utilization (%)"; };
plugin = {
kind = "TimeSeriesChart";
spec = {
queries = [{
kind = "TimeSeriesQuery";
spec = {
plugin = {
kind = "PrometheusTimeSeriesQuery";
spec = {
query = "100 - (avg by (instance) (rate(node_cpu_seconds_total{mode='idle'}[1m])) * 100)";
};
};
};
}];
};
};
};
};
memoryArc = {
kind = "Panel";
spec = {
display = { name = "Memory & ARC Cache (Bytes)"; };
plugin = {
kind = "TimeSeriesChart";
spec = {
queries = [
{
kind = "TimeSeriesQuery";
spec = {
plugin = {
kind = "PrometheusTimeSeriesQuery";
spec = {
query = "node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes";
};
};
};
}
];
};
};
};
};
diskIO = {
kind = "Panel";
spec = {
display = { name = "ZFS Pool Read/Write Throughput"; };
plugin = {
kind = "TimeSeriesChart";
spec = {
queries = [{
kind = "TimeSeriesQuery";
spec = {
plugin = {
kind = "PrometheusTimeSeriesQuery";
spec = {
query = "rate(node_disk_read_bytes_total[1m]) + rate(node_disk_written_bytes_total[1m])";
};
};
};
}];
};
};
};
};
};
layouts = [
{
kind = "Grid";
spec = {
items = [
{ x = 0; y = 0; width = 12; height = 6; content = { "$ref" = "#/spec/panels/cpuUsage"; }; }
{ x = 12; y = 0; width = 12; height = 6; content = { "$ref" = "#/spec/panels/memoryArc"; }; }
{ x = 0; y = 6; width = 24; height = 8; content = { "$ref" = "#/spec/panels/diskIO"; }; }
];
};
}
];
};
});
persesConfigFile = pkgs.writeText "perses-config.yaml" ''
database:
file:
folder: "/var/lib/perses"
extension: "json"
schemas:
panels_path: "/var/lib/perses/schemas/panels"
queries_path: "/var/lib/perses/schemas/queries"
datasources_path: "/var/lib/perses/schemas/datasources"
variables_path: "/var/lib/perses/schemas/variables"
security:
readonly: false
enable_auth: false
'';
in {
options.services.naxos.perses = {
enable = mkEnableOption "Perses Embedded Analytics Engine";
port = mkOption {
type = types.port;
default = 8080;
description = "Perses dashboard server port.";
};
};
config = mkIf cfg.enable {
# System user for Perses
users.users.perses = {
isSystemUser = true;
group = "perses";
home = "/var/lib/perses";
createHome = true;
};
users.groups.perses = {};
# Systemd service for Perses
systemd.services.perses = {
description = "Perses Native Observability and Dashboard Service";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" "prometheus.service" ];
serviceConfig = {
User = "perses";
Group = "perses";
StateDirectory = "perses";
WorkingDirectory = "/var/lib/perses";
ExecStartPre = pkgs.writeShellScript "perses-provisioning" ''
mkdir -p /var/lib/perses/projects/naxos/dashboards
mkdir -p /var/lib/perses/projects/naxos/datasources
cp -f ${datasourceManifest} /var/lib/perses/projects/naxos/datasources/PrometheusDemo.json
cp -f ${zfsDashboardManifest} /var/lib/perses/projects/naxos/dashboards/zfs-storage-health.json
'';
ExecStart = "${pkgs.perses or pkgs.prometheus}/bin/perses --config=${persesConfigFile} --port=${toString cfg.port}";
Restart = "always";
RestartSec = "5s";
};
};
networking.firewall.allowedTCPPorts = [ cfg.port ];
};
}
+72
View File
@@ -0,0 +1,72 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.naxos.monitoring;
in {
options.services.naxos.monitoring = {
enable = mkEnableOption "NaxOS Metrics and Telemetry Collection";
prometheusPort = mkOption {
type = types.port;
default = 9090;
description = "Prometheus server listen port.";
};
nodeExporterPort = mkOption {
type = types.port;
default = 9100;
description = "Node exporter listen port.";
};
retentionTime = mkOption {
type = types.str;
default = "15d";
description = "Metrics retention period.";
};
};
config = mkIf cfg.enable {
# Prometheus TSDB
services.prometheus = {
enable = true;
port = cfg.prometheusPort;
retentionTime = cfg.retentionTime;
extraFlags = [
"--web.enable-remote-write-receiver"
];
scrapeConfigs = [
{
job_name = "node";
static_configs = [{
targets = [ "127.0.0.1:${toString cfg.nodeExporterPort}" ];
}];
}
{
job_name = "naxos-api";
static_configs = [{
targets = [ "127.0.0.1:8088" ];
}];
}
];
};
# Node exporter for hardware & OS metrics
services.prometheus.exporters.node = {
enable = true;
port = cfg.nodeExporterPort;
enabledCollectors = [
"cpu"
"diskstats"
"filesystem"
"loadavg"
"meminfo"
"netdev"
"stat"
"systemd"
"thermal_zone"
];
};
};
}
+110
View File
@@ -0,0 +1,110 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.naxos.appEngine;
in {
options.services.naxos.appEngine = {
enable = mkEnableOption "NaxOS Multi-Tier App Engine";
defaultRuntime = mkOption {
type = types.enum [ "systemd" "docker" "k3s" ];
default = "docker";
description = "Default execution runtime for user workloads.";
};
docker = {
enable = mkOption {
type = types.bool;
default = true;
description = "Enable Docker engine for containerized applications.";
};
storageDriver = mkOption {
type = types.enum [ "zfs" "overlay2" "btrfs" ];
default = "zfs";
description = "Container storage driver. 'zfs' uses native copy-on-write datasets.";
};
dataRoot = mkOption {
type = types.str;
default = "/var/lib/docker";
description = "Storage root for container images and layers.";
};
};
k3s = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable lightweight K3s single-node cluster for cloud-native orchestration.";
};
role = mkOption {
type = types.enum [ "server" "agent" ];
default = "server";
description = "K3s node role.";
};
tokenFile = mkOption {
type = types.nullOr types.str;
default = null;
description = "Path to token file for K3s node join / cluster security.";
};
};
gpuAcceleration = {
enable = mkOption {
type = types.bool;
default = true;
description = "Enable hardware transcoding and machine learning acceleration.";
};
vendor = mkOption {
type = types.enum [ "intel" "nvidia" "amd" "none" ];
default = "intel";
description = "Primary GPU hardware vendor.";
};
};
};
config = mkIf cfg.enable {
# 1. Docker Runtime Configuration
virtualisation.docker = mkIf cfg.docker.enable {
enable = true;
storageDriver = cfg.docker.storageDriver;
daemon.settings = {
data-root = cfg.docker.dataRoot;
log-driver = "journald";
};
};
# OCI container backend compatibility
virtualisation.oci-containers.backend = mkIf cfg.docker.enable "docker";
# 2. K3s Runtime Configuration
services.k3s = mkIf cfg.k3s.enable {
enable = true;
role = cfg.k3s.role;
tokenFile = cfg.k3s.tokenFile;
extraFlags = toString [
"--disable=traefik" # We manage ingress/reverse-proxy through NaxOS
"--snapshotter=native"
];
};
# 3. Hardware Graphics Acceleration
hardware.graphics = mkIf cfg.gpuAcceleration.enable {
enable = true;
extraPackages = mkIf (cfg.gpuAcceleration.vendor == "intel") (with pkgs; [
intel-media-driver # Broadwell or newer
intel-compute-runtime # OpenCL support
vpl-gpu-rt # QSV support (11th Gen+)
]);
};
# 4. System packages for workload operations
environment.systemPackages = with pkgs; [
docker-compose
lazydocker
kubectl
dive
];
};
}
+11
View File
@@ -0,0 +1,11 @@
{ ... }:
{
imports = [
./immich.nix
./nextcloud.nix
./jellyfin.nix
./paperless.nix
./vaultwarden.nix
];
}
+84
View File
@@ -0,0 +1,84 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.naxos.apps.immich;
in {
options.services.naxos.apps.immich = {
enable = mkEnableOption "Immich Self-Hosted Photo & Video Hub";
runtime = mkOption {
type = types.enum [ "systemd" "docker" ];
default = "systemd";
description = "Runtime to execute Immich (systemd for native bare-metal speed with NixOS package, docker for containerized).";
};
port = mkOption {
type = types.port;
default = 2283;
description = "Web interface and API port.";
};
host = mkOption {
type = types.str;
default = "0.0.0.0";
description = "Listen host.";
};
mediaLocation = mkOption {
type = types.str;
default = "/tank/media/photos";
description = "ZFS dataset or path where uploaded photos and videos are stored.";
};
accelerationDevices = mkOption {
type = types.listOf types.str;
default = [ "/dev/dri/renderD128" ];
description = "DRM render devices for hardware-accelerated transcoding.";
};
machineLearningCPUQuota = mkOption {
type = types.str;
default = "200%";
description = "CPU quota for machine learning service (200% = 2 full cores).";
};
openFirewall = mkOption {
type = types.bool;
default = true;
description = "Open port in firewall.";
};
};
config = mkIf cfg.enable {
# Systemd / Native NixOS Deployment
services.immich = mkIf (cfg.runtime == "systemd") {
enable = true;
host = cfg.host;
port = cfg.port;
mediaLocation = cfg.mediaLocation;
openFirewall = cfg.openFirewall;
accelerationDevices = cfg.accelerationDevices;
};
services.redis.servers.immich = mkIf (cfg.runtime == "systemd") {
logLevel = "warning";
};
systemd.services.immich-machine-learning = mkIf (cfg.runtime == "systemd") {
serviceConfig = {
CPUQuota = cfg.machineLearningCPUQuota;
Nice = 19;
};
};
users.users.immich = mkIf (cfg.runtime == "systemd") {
extraGroups = [ "video" "render" ];
};
systemd.tmpfiles.rules = [
"d ${cfg.mediaLocation} 0750 immich immich -"
];
};
}
+33
View File
@@ -0,0 +1,33 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.naxos.apps.jellyfin;
in {
options.services.naxos.apps.jellyfin = {
enable = mkEnableOption "Jellyfin Open-Source Media Streaming Server";
openFirewall = mkOption {
type = types.bool;
default = true;
description = "Open port 8096 in firewall.";
};
user = mkOption {
type = types.str;
default = "jellyfin";
description = "Service user.";
};
};
config = mkIf cfg.enable {
services.jellyfin = {
enable = true;
openFirewall = cfg.openFirewall;
user = cfg.user;
};
users.users.${cfg.user}.extraGroups = [ "video" "render" ];
};
}
+43
View File
@@ -0,0 +1,43 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.naxos.apps.nextcloud;
in {
options.services.naxos.apps.nextcloud = {
enable = mkEnableOption "Nextcloud Personal Cloud & Collaboration Platform";
hostName = mkOption {
type = types.str;
default = "cloud.local";
description = "Domain / hostname for Nextcloud.";
};
homeDir = mkOption {
type = types.str;
default = "/tank/data/nextcloud";
description = "Persistent data directory on ZFS storage.";
};
adminpassFile = mkOption {
type = types.nullOr types.str;
default = null;
description = "Path to file containing initial admin password.";
};
};
config = mkIf cfg.enable {
services.nextcloud = {
enable = true;
hostName = cfg.hostName;
home = cfg.homeDir;
config = {
adminuser = "admin";
adminpassFile = cfg.adminpassFile;
dbtype = "sqlite";
};
caching.redis = true;
};
};
}
+55
View File
@@ -0,0 +1,55 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.naxos.apps.paperless;
in {
options.services.naxos.apps.paperless = {
enable = mkEnableOption "Paperless-ngx Document Archiving System";
port = mkOption {
type = types.port;
default = 28981;
description = "Web interface port.";
};
mediaDir = mkOption {
type = types.str;
default = "/tank/data/paperless/media";
description = "Directory where archived documents and OCR results are stored.";
};
consumptionDir = mkOption {
type = types.str;
default = "/tank/scans";
description = "Ingestion directory where scanner uploads incoming documents.";
};
openFirewall = mkOption {
type = types.bool;
default = true;
description = "Open port in firewall.";
};
};
config = mkIf cfg.enable {
services.paperless = {
enable = true;
port = cfg.port;
mediaDir = cfg.mediaDir;
consumptionDir = cfg.consumptionDir;
settings = {
PAPERLESS_OCR_LANGUAGE = "deu+eng";
PAPERLESS_CONSUMER_POLLING = 30;
};
};
networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
systemd.tmpfiles.rules = [
"d ${cfg.mediaDir} 0750 paperless paperless -"
"d ${cfg.consumptionDir} 0775 paperless users -"
];
};
}
+35
View File
@@ -0,0 +1,35 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.naxos.apps.vaultwarden;
in {
options.services.naxos.apps.vaultwarden = {
enable = mkEnableOption "Vaultwarden Password & Secret Vault";
port = mkOption {
type = types.port;
default = 8222;
description = "Web interface port.";
};
openFirewall = mkOption {
type = types.bool;
default = true;
description = "Open port in firewall.";
};
};
config = mkIf cfg.enable {
services.vaultwarden = {
enable = true;
config = {
ROCKET_PORT = cfg.port;
ROCKET_ADDRESS = "0.0.0.0";
};
};
networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ];
};
}
+72
View File
@@ -0,0 +1,72 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.naxos.shares.nfs;
in {
options.services.naxos.shares.nfs = {
enable = mkEnableOption "NaxOS Declarative NFS Service";
lockdPort = mkOption {
type = types.int;
default = 4001;
description = "Port for lockd.";
};
mountdPort = mkOption {
type = types.int;
default = 4002;
description = "Port for mountd.";
};
exports = mkOption {
type = types.listOf (types.submodule {
options = {
path = mkOption {
type = types.str;
description = "Path to exported directory or dataset.";
};
clients = mkOption {
type = types.listOf (types.submodule {
options = {
subnet = mkOption {
type = types.str;
example = "10.0.0.0/23";
description = "Allowed client subnet or IP.";
};
options = mkOption {
type = types.str;
default = "rw,sync,no_subtree_check,no_root_squash";
description = "NFS export options.";
};
};
});
default = [];
description = "Clients allowed to mount this export.";
};
};
});
default = [];
description = "List of NFS exported directories.";
};
};
config = mkIf cfg.enable {
services.nfs.server = {
enable = true;
lockdPort = cfg.lockdPort;
mountdPort = cfg.mountdPort;
exports = concatMapStringsSep "\n" (exp:
let
clientList = concatMapStringsSep " " (c: "${c.subnet}(${c.options})") exp.clients;
in "${exp.path} ${clientList}"
) cfg.exports;
};
networking.firewall = {
allowedTCPPorts = [ 111 2049 cfg.lockdPort cfg.mountdPort ];
allowedUDPPorts = [ 111 2049 cfg.lockdPort cfg.mountdPort ];
};
};
}
+197
View File
@@ -0,0 +1,197 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.naxos.shares.samba;
in {
options.services.naxos.shares.samba = {
enable = mkEnableOption "NaxOS Declarative Samba (SMB) Service";
workgroup = mkOption {
type = types.str;
default = "WORKGROUP";
description = "NetBIOS workgroup.";
};
serverString = mkOption {
type = types.str;
default = "NaxOS Storage Appliance";
description = "Server announcement string.";
};
netbiosName = mkOption {
type = types.str;
default = "naxos";
description = "NetBIOS hostname.";
};
allowedHosts = mkOption {
type = types.listOf types.str;
default = [ "10.0.0." "192.168." "172.16." "127.0.0.1" "localhost" ];
description = "Allowed IP subnets or hosts for SMB access.";
};
shares = mkOption {
type = types.attrsOf (types.submodule {
options = {
enable = mkOption {
type = types.bool;
default = true;
description = "Whether to publish this share.";
};
path = mkOption {
type = types.str;
description = "Target local directory or ZFS mountpoint.";
};
comment = mkOption {
type = types.str;
default = "";
description = "Share description.";
};
readOnly = mkOption {
type = types.bool;
default = false;
description = "Whether the share is read-only.";
};
browseable = mkOption {
type = types.bool;
default = true;
description = "Whether the share is visible in network browsing.";
};
guestOk = mkOption {
type = types.bool;
default = false;
description = "Whether anonymous/guest access is allowed.";
};
validUsers = mkOption {
type = types.listOf types.str;
default = [];
description = "List of valid users or @groups.";
};
forceUser = mkOption {
type = types.nullOr types.str;
default = null;
description = "Force UNIX user for all connections.";
};
forceGroup = mkOption {
type = types.nullOr types.str;
default = null;
description = "Force UNIX group for all connections.";
};
createMask = mkOption {
type = types.str;
default = "0664";
description = "File creation permission mask.";
};
directoryMask = mkOption {
type = types.str;
default = "0775";
description = "Directory creation permission mask.";
};
timeMachine = mkOption {
type = types.bool;
default = false;
description = "Enable Apple Time Machine compatibility mode (vfs_fruit).";
};
timeMachineMaxSize = mkOption {
type = types.nullOr types.str;
default = null;
example = "512G";
description = "Quota size limit advertised to Time Machine.";
};
};
});
default = {};
description = "Declaratively managed SMB shares.";
};
};
config = mkIf cfg.enable {
services.samba = {
enable = true;
openFirewall = true;
settings = {
global = {
workgroup = cfg.workgroup;
"server string" = cfg.serverString;
"netbios name" = cfg.netbiosName;
security = "user";
"hosts allow" = concatStringsSep " " cfg.allowedHosts;
"hosts deny" = "0.0.0.0/0";
"guest account" = "nobody";
"map to guest" = "bad user";
# Apple & ZFS ACL compatibility
"vfs objects" = "catia fruit streams_xattr acl_xattr";
"fruit:aapl" = "yes";
"fruit:metadata" = "stream";
"fruit:model" = "Macmini";
"fruit:posix_rename" = "yes";
"fruit:zero_file_id" = "yes";
# ACLs and inheritance
"map acl inherit" = "yes";
"inherit acls" = "yes";
"ea support" = "yes";
};
} // (mapAttrs' (shareName: shareCfg:
nameValuePair shareName (
{
path = shareCfg.path;
comment = shareCfg.comment;
browseable = if shareCfg.browseable then "yes" else "no";
"read only" = if shareCfg.readOnly then "yes" else "no";
"guest ok" = if shareCfg.guestOk then "yes" else "no";
"create mask" = shareCfg.createMask;
"directory mask" = shareCfg.directoryMask;
}
// optionalAttrs (shareCfg.validUsers != []) {
"valid users" = concatStringsSep " " shareCfg.validUsers;
}
// optionalAttrs (shareCfg.forceUser != null) {
"force user" = shareCfg.forceUser;
}
// optionalAttrs (shareCfg.forceGroup != null) {
"force group" = shareCfg.forceGroup;
}
// optionalAttrs shareCfg.timeMachine {
"vfs objects" = "catia fruit streams_xattr acl_xattr";
"fruit:time machine" = "yes";
}
// optionalAttrs (shareCfg.timeMachine && shareCfg.timeMachineMaxSize != null) {
"fruit:time machine max size" = shareCfg.timeMachineMaxSize;
}
)
) (filterAttrs (n: v: v.enable) cfg.shares));
};
# Enable Avahi (mDNS / Bonjour) for automatic SMB & Time Machine discovery on macOS/iOS/Windows
services.avahi = {
enable = true;
nssmdns4 = true;
publish = {
enable = true;
userServices = true;
};
extraServiceFiles = {
smb = ''
<?xml version="1.0" standalone='no'?><!--*-nxml-*-->
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
<service-group>
<name replace-wildcards="yes">%h (NaxOS SMB)</name>
<service>
<type>_smb._tcp</type>
<port>445</port>
</service>
<service>
<type>_device-info._tcp</type>
<port>0</port>
<txt-record>model=RackMac</txt-record>
</service>
</service-group>
'';
};
};
};
}
+222
View File
@@ -0,0 +1,222 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.naxos.storage;
in {
options.services.naxos.storage = {
enable = mkEnableOption "NaxOS Declarative ZFS Storage Engine";
arcMaxBytes = mkOption {
type = types.nullOr types.ints.positive;
default = 4294967296; # 4 GiB default
description = "Maximum ZFS ARC cache size in bytes.";
};
autoScrub = {
enable = mkOption {
type = types.bool;
default = true;
description = "Enable automated regular scrubbing of ZFS pools.";
};
interval = mkOption {
type = types.str;
default = "monthly";
description = "Interval for auto-scrub (e.g. monthly, weekly).";
};
};
autoTrim = {
enable = mkOption {
type = types.bool;
default = true;
description = "Enable periodic TRIM for SSD/NVMe vdevs.";
};
};
autoSnapshot = {
enable = mkOption {
type = types.bool;
default = true;
description = "Enable automatic snapshot retention policies.";
};
hourly = mkOption {
type = types.int;
default = 24;
description = "Number of hourly snapshots to keep.";
};
daily = mkOption {
type = types.int;
default = 7;
description = "Number of daily snapshots to keep.";
};
weekly = mkOption {
type = types.int;
default = 4;
description = "Number of weekly snapshots to keep.";
};
monthly = mkOption {
type = types.int;
default = 12;
description = "Number of monthly snapshots to keep.";
};
};
importExistingPools = mkOption {
type = types.listOf types.str;
default = [];
example = [ "tank" "datapool" ];
description = "List of existing foreign ZFS pools (e.g. from TrueNAS or nixos-lukas) to safely import without formatting.";
};
pools = mkOption {
type = types.attrsOf (types.submodule {
options = {
enable = mkOption {
type = types.bool;
default = true;
description = "Whether to manage this pool.";
};
ashift = mkOption {
type = types.int;
default = 12;
description = "ZFS vdev ashift alignment value (12 = 4K sectors).";
};
layout = mkOption {
type = types.enum [ "stripe" "mirror" "raidz1" "raidz2" "raidz3" ];
default = "mirror";
description = "Vdev topology layout.";
};
devices = mkOption {
type = types.listOf types.str;
default = [];
example = [ "/dev/disk/by-id/nvme-..." "/dev/disk/by-id/ata-..." ];
description = "Member disk devices or partitions.";
};
datasets = mkOption {
type = types.attrsOf (types.submodule {
options = {
mountpoint = mkOption {
type = types.nullOr types.str;
default = null;
description = "Mountpoint for this dataset. If null, uses default ZFS mount.";
};
compression = mkOption {
type = types.enum [ "on" "off" "lz4" "zstd" "zstd-fast" "gzip" ];
default = "lz4";
description = "ZFS compression algorithm.";
};
recordsize = mkOption {
type = types.str;
default = "128K";
description = "Record size for dataset (e.g. 1M for media, 16K for databases).";
};
quota = mkOption {
type = types.nullOr types.str;
default = null;
description = "Optional dataset quota (e.g. 500G).";
};
reservation = mkOption {
type = types.nullOr types.str;
default = null;
description = "Optional dataset reservation.";
};
owner = mkOption {
type = types.str;
default = "root";
description = "POSIX owner of mountpoint.";
};
group = mkOption {
type = types.str;
default = "root";
description = "POSIX group of mountpoint.";
};
mode = mkOption {
type = types.str;
default = "0755";
description = "POSIX directory mode permissions.";
};
};
});
default = {};
description = "Sub-datasets belonging to this pool.";
};
};
});
default = {};
description = "Declaratively defined ZFS pools and datasets.";
};
};
config = mkIf cfg.enable {
boot.supportedFilesystems = [ "zfs" ];
boot.kernelPackages = pkgs.linuxPackages;
# Kernel parameter for ZFS ARC Max limit
boot.kernelParams = mkIf (cfg.arcMaxBytes != null) [
"zfs.zfs_arc_max=${toString cfg.arcMaxBytes}"
];
# Safe pool import list (includes both configured pools and imported existing pools)
boot.zfs.extraPools = unique (
(attrNames (filterAttrs (n: v: v.enable) cfg.pools)) ++
cfg.importExistingPools
);
boot.zfs.forceImportRoot = false;
# Automated ZFS services
services.zfs = {
autoScrub = {
enable = cfg.autoScrub.enable;
interval = cfg.autoScrub.interval;
};
trim = {
enable = cfg.autoTrim.enable;
};
autoSnapshot = {
enable = cfg.autoSnapshot.enable;
hourly = cfg.autoSnapshot.hourly;
daily = cfg.autoSnapshot.daily;
weekly = cfg.autoSnapshot.weekly;
monthly = cfg.autoSnapshot.monthly;
};
};
# System tools for storage diagnosis and disk monitoring
environment.systemPackages = with pkgs; [
zfs
smartmontools
hdparm
nvme-cli
parted
gptfdisk
iotop
ncdu
sanoid
syncoid
];
# Generate systemd.tmpfiles rules for configured datasets with custom owners/modes
systemd.tmpfiles.rules = flatten (mapAttrsToList (poolName: poolCfg:
mapAttrsToList (dsName: dsCfg:
mkIf (dsCfg.mountpoint != null)
"d ${dsCfg.mountpoint} ${dsCfg.mode} ${dsCfg.owner} ${dsCfg.group} -"
) poolCfg.datasets
) cfg.pools);
# Generate fileSystems definitions for datasets with explicit mountpoints
fileSystems = foldl' (acc: pool:
let
poolName = pool.name;
poolCfg = pool.value;
in acc // (mapAttrs' (dsName: dsCfg:
nameValuePair dsCfg.mountpoint {
device = "${poolName}/${dsName}";
fsType = "zfs";
options = [ "nofail" ];
}
) (filterAttrs (n: v: v.mountpoint != null) poolCfg.datasets))
) {} (mapAttrsToList (name: value: { inherit name value; }) (filterAttrs (n: v: v.enable) cfg.pools));
};
}
+72
View File
@@ -0,0 +1,72 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.naxos.ui;
in {
options.services.naxos.ui = {
enable = mkEnableOption "NaxOS Web Dashboard Service";
port = mkOption {
type = types.port;
default = 80;
description = "Web interface listen port.";
};
sslPort = mkOption {
type = types.port;
default = 443;
description = "SSL/HTTPS listen port.";
};
staticPath = mkOption {
type = types.str;
default = "/opt/naxos/ui/dist";
description = "Path to compiled React/Vite web dashboard assets.";
};
};
config = mkIf cfg.enable {
services.nginx = {
enable = true;
recommendedProxySettings = true;
recommendedGzipSettings = true;
recommendedOptimisation = true;
virtualHosts."naxos.local" = {
default = true;
listen = [
{ addr = "0.0.0.0"; port = cfg.port; }
{ addr = "[::]"; port = cfg.port; }
];
# Serve Frontend SPA
locations."/" = {
root = cfg.staticPath;
tryFiles = "$uri $uri/ /index.html";
};
# Reverse Proxy to NaxOS API & WebSocket / SSE
locations."/api/" = {
proxyPass = "http://127.0.0.1:${toString config.services.naxos.api.port}/";
proxyWebsockets = true;
extraConfig = ''
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
'';
};
# Reverse Proxy to Perses Embedded Analytics
locations."/perses/" = {
proxyPass = "http://127.0.0.1:${toString config.services.naxos.perses.port}/";
proxyWebsockets = true;
};
};
};
networking.firewall.allowedTCPPorts = [ cfg.port cfg.sslPort ];
};
}