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
+46
View File
@@ -0,0 +1,46 @@
name: Build NaxOS Bootable ISO
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
channel:
description: 'Target Release Channel'
required: true
default: 'stable'
jobs:
build-iso:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Nix with Flake Support
uses: cachix/install-nix-action@v27
with:
nix_path: nixpkgs=channel:nixos-24.11
extra_nix_config: |
experimental-features = nix-command flakes
- name: Build Bootable NaxOS Installer ISO
run: |
nix build .#iso --show-trace
- name: Locate ISO Artifact
id: iso-info
run: |
ISO_FILE=$(find result/iso -name "*.iso" | head -n 1)
echo "Found ISO: $ISO_FILE"
SHA256=$(sha256sum "$ISO_FILE" | cut -d' ' -f1)
echo "SHA256: $SHA256"
echo "iso_file=$ISO_FILE" >> $GITHUB_OUTPUT
echo "sha256=$SHA256" >> $GITHUB_OUTPUT
- name: Upload ISO Artifact
uses: actions/upload-artifact@v4
with:
name: naxos-installer-iso
path: result/iso/*.iso
+39
View File
@@ -0,0 +1,39 @@
name: Package NaxOS Release
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Nix
uses: cachix/install-nix-action@v27
with:
extra_nix_config: |
experimental-features = nix-command flakes
- name: Build ISO
run: |
nix build .#iso
- name: Generate Checksums
run: |
mkdir -p release-dist
cp result/iso/*.iso release-dist/
cd release-dist
sha256sum *.iso > SHA256SUMS
- name: Create Gitea Release
uses: softprops/action-gh-release@v2
with:
files: |
release-dist/*
draft: false
prerelease: false
generate_release_notes: true
+29
View File
@@ -0,0 +1,29 @@
name: Test NaxOS Module Configurations
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test-modules:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Install Nix
uses: cachix/install-nix-action@v27
with:
extra_nix_config: |
experimental-features = nix-command flakes
- name: Evaluate Flake Outputs & Check Syntax
run: |
nix flake show
nix flake check --all-systems
- name: Validate Appliance System Configuration Evaluation
run: |
nix eval .#nixosConfigurations.naxos.config.system.build.toplevel.drvPath
+57 -2
View File
@@ -1,3 +1,58 @@
# naxos-os # NaxOS Core (`naxos-os`)
NaxOS Operating System - Declarative NixOS Core, Modules, ISO Installer, and Appliance Architecture [![Gitea Actions](https://git.lholz.de/naxos/naxos-os/actions/workflows/test-modules.yaml/badge.svg)](https://git.lholz.de/naxos/naxos-os/actions)
**NaxOS** is a declarative, appliance-like Network Attached Storage (NAS) operating system built on top of **NixOS** and **OpenZFS**.
## Architecture & Features
- **Declarative NixOS Foundation**: Complete system state defined as code, from kernel sysctl parameters to users and storage configurations.
- **GitOps-Driven Rebuilds**: Automated `nixos-rebuild switch` with dry-build safety checks, canary testing, and instant rollback.
- **Native OpenZFS Management**:
- Declarative pools (mirror, raidz1, raidz2, stripe)
- Declarative datasets with per-dataset quotas, compression (`lz4`, `zstd`), and recordsize tuning
- First-class zero-data-loss import of existing pools (e.g. from TrueNAS or raw NixOS setups like `nixos-lukas`)
- Automated scrub, TRIM, and snapshot lifecycle policies (hourly, daily, weekly, monthly)
- **Multi-Protocol File Shares**:
- **Samba**: Preconfigured with macOS Time Machine support (`vfs_fruit`), POSIX ACL propagation, and fine-grained share permissions.
- **NFS**: High-throughput NFS exports with CIDR client filters.
- **Multi-Tier Workload & App Engine**:
- Native NixOS systemd services (e.g. Immich with Intel QuickSync / OpenCL hardware transcoding)
- Docker Compose with ZFS copy-on-write storage driver
- Optional lightweight K3s cluster integration
- **Native Observability**:
- Embedded **Perses** analytics dashboards (replacing standalone Grafana)
- Prometheus TSDB and Node Exporter
- **Streamlined Bootable ISO**:
- Interactive CLI / TUI wizard (`naxos-installer`) for rapid bare-metal installation and pool import.
## Repository Structure
```
naxos-os/
├── flake.nix # Flake exports (modules, configurations, ISO builder)
├── modules/ # NaxOS module definitions
│ ├── core/ # System base, users, gitops, self-update
│ ├── storage/ # ZFS pool/dataset management & migration
│ ├── shares/ # Samba (SMB) and NFS file shares
│ ├── services/ # App engine runtime & application catalog
│ ├── monitoring/ # Prometheus & Perses analytics engine
│ ├── api/ # Management daemon systemd service
│ └── ui/ # Web dashboard Nginx reverse proxy service
├── profiles/ # Hardware & appliance base profiles
├── iso/ # Bootable installer ISO & scripts
└── .gitea/workflows/ # Gitea Actions CI/CD workflows
```
## Quick Start
### Building the Installation ISO
```bash
nix build .#iso
```
### Testing System Evaluation
```bash
nix flake check
nix eval .#nixosConfigurations.naxos.config.system.build.toplevel.drvPath
```
+53
View File
@@ -0,0 +1,53 @@
{
description = "NaxOS: Declarative, Appliance-Like NixOS- & ZFS-Based Network Attached Storage";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixos-unstable";
};
outputs = { self, nixpkgs, nixpkgs-unstable, ... }:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
};
in {
# Exported NixOS Modules for NaxOS
nixosModules = {
naxos = import ./modules;
default = self.nixosModules.naxos;
};
# Standard NaxOS Appliance System Configuration
nixosConfigurations = {
naxos = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
self.nixosModules.naxos
./profiles/nas-appliance.nix
./profiles/hardware-generic.nix
];
};
# Bootable Installer ISO
installer-iso = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
./iso/installer-iso.nix
];
};
};
# Exported Packages & ISO Image
packages.${system} = {
iso = self.nixosConfigurations.installer-iso.config.system.build.isoImage;
default = pkgs.writeShellScriptBin "naxos-help" ''
echo "NaxOS Appliance Flake"
echo "To build installer ISO: nix build .#iso"
echo "To test appliance configuration: nix build .#nixosConfigurations.naxos.config.system.build.toplevel"
'';
};
};
}
+62
View File
@@ -0,0 +1,62 @@
{ pkgs, lib, ... }:
{
# Minimal Live ISO base
imports = [
<nixpkgs/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix>
];
# ZFS and Storage tooling on Live ISO
boot.supportedFilesystems = [ "zfs" ];
boot.kernelPackages = pkgs.linuxPackages;
environment.systemPackages = with pkgs; [
zfs
parted
gptfdisk
e2fsprogs
dosfstools
smartmontools
curl
wget
git
jq
tmux
htop
btop
whiptail
dialog
];
# Automatically launch installer wizard on tty1
services.getty.autologinUser = "root";
# Welcome banner and installer prompt in bash profile
environment.etc."issue".text = ''
========================================================
Welcome to NaxOS Installation Media
========================================================
To launch the interactive installer wizard, run:
naxos-installer
To inspect disks and ZFS pools manually:
lsblk
zpool import
========================================================
'';
# Link installer script into PATH
system.activationScripts.naxosInstallerScript = ''
cp -f ${./scripts/naxos-installer.sh} /usr/local/bin/naxos-installer || true
chmod +x /usr/local/bin/naxos-installer || true
'';
# ISO Image identification
isoImage.isoBaseName = "naxos-installer";
isoImage.volumeID = "NAXOS_INSTALL";
isoImage.makeEfiBootable = true;
isoImage.makeUsbBootable = true;
# State version
system.stateVersion = "24.11";
}
+193
View File
@@ -0,0 +1,193 @@
#!/usr/bin/env bash
# ==============================================================================
# NaxOS Automated & Interactive Installer Wizard
# Guides user through disk partitioning, ZFS pool creation or safe foreign pool
# import (TrueNAS / nixos-lukas), network setup, and initial credentials.
# ==============================================================================
set -euo pipefail
RED='\033[0;31m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'
clear
cat << "EOF"
_ _ ___ ____
| \ | | __ ___ __/ _ \/ ___|
| \| |/ _` \ \/ / | | \___ \
| |\ | (_| |> <| |_| |___) |
|_| \_|\__,_/_/\_\\___/|____/
Declarative NixOS & ZFS Storage Appliance
================================================
EOF
echo -e "${BOLD}${CYAN}Welcome to the NaxOS Installation Wizard!${NC}\n"
# 1. Hardware Check
echo -e "${BOLD}[1/6] Scanning Hardware & Storage Devices...${NC}"
TOTAL_RAM_MB=$(free -m | awk '/^Mem:/{print $2}')
CPU_CORES=$(nproc)
echo " Detected CPU Cores: $CPU_CORES"
echo " Detected RAM: ${TOTAL_RAM_MB} MB"
echo -e "\nAvailable Physical Disks:"
lsblk -d -p -n -o NAME,SIZE,MODEL,TRAN | grep -v 'loop' || true
# 2. Storage Pool Setup (New vs Import)
echo -e "\n${BOLD}[2/6] Storage Architecture Configuration${NC}"
echo "1) Create a brand new ZFS storage pool (Formatted & optimized for NAS)"
echo "2) Import an EXISTING ZFS pool (Preserve existing data e.g. TrueNAS or nixos-lukas 'tank')"
echo "3) Skip storage configuration (Configure post-boot via Web Dashboard)"
read -rp "Select option [1-3] (default: 2): " STORAGE_OPTION
STORAGE_OPTION=${STORAGE_OPTION:-2}
POOL_NAME="tank"
IMPORTED_POOL=""
CREATE_POOL=false
if [ "$STORAGE_OPTION" = "1" ]; then
CREATE_POOL=true
read -rp "Enter new pool name (default: tank): " POOL_INPUT
POOL_NAME=${POOL_INPUT:-tank}
echo "Select pool layout: [1] Mirror, [2] RAID-Z1, [3] RAID-Z2, [4] Single/Stripe"
read -rp "Layout (default: 1): " LAYOUT_INPUT
LAYOUT_INPUT=${LAYOUT_INPUT:-1}
read -rp "Enter disk devices space-separated (e.g. /dev/sdb /dev/sdc): " DISK_DEVICES
elif [ "$STORAGE_OPTION" = "2" ]; then
echo -e "\n${BLUE}Scanning for unimported ZFS pools on connected drives...${NC}"
zpool import || true
read -rp "Enter the name of the existing pool to import (e.g. tank): " IMPORTED_POOL
if [ -n "$IMPORTED_POOL" ]; then
echo -e "${GREEN}Verifying pool '$IMPORTED_POOL' status safely without mounting...${NC}"
zpool import -N -f "$IMPORTED_POOL" || echo "Warning: Pool could not be imported immediately. Will configure for boot-time import."
fi
fi
# 3. Target System OS Disk Selection
echo -e "\n${BOLD}[3/6] Target OS Disk Selection${NC}"
echo "Enter the disk where NaxOS system files should be installed (WARNING: this disk will be formatted!):"
read -rp "OS Disk (e.g. /dev/sda or /dev/nvme0n1): " OS_DISK
if [ -z "$OS_DISK" ] || [ ! -b "$OS_DISK" ]; then
echo -e "${RED}Error: Disk '$OS_DISK' does not exist! Aborting.${NC}"
exit 1
fi
echo -e "${RED}${BOLD}WARNING: All data on $OS_DISK will be destroyed!${NC}"
read -rp "Are you sure you want to proceed with $OS_DISK? (yes/no): " CONFIRM
if [ "$CONFIRM" != "yes" ]; then
echo "Installation canceled by user."
exit 0
fi
# 4. Network Configuration
echo -e "\n${BOLD}[4/6] Network Configuration${NC}"
read -rp "Appliance Hostname (default: naxos): " HOSTNAME
HOSTNAME=${HOSTNAME:-naxos}
read -rp "Use DHCP for network? (yes/no, default: yes): " USE_DHCP
USE_DHCP=${USE_DHCP:-yes}
# 5. Admin User & Credentials
echo -e "\n${BOLD}[5/6] Administrator Account & Access${NC}"
read -rp "Admin username (default: admin): " ADMIN_USER
ADMIN_USER=${ADMIN_USER:-admin}
read -rp "Paste Admin SSH Public Key (optional): " ADMIN_SSH_KEY
# 6. Partitioning and Installation
echo -e "\n${BOLD}[6/6] Executing Partitioning & Installation...${NC}"
# Partition OS disk: 1GB EFI, remainder for root
wipefs -a "$OS_DISK"
parted -s "$OS_DISK" -- mklabel gpt
parted -s "$OS_DISK" -- mkpart ESP fat32 1MiB 1024MiB
parted -s "$OS_DISK" -- set 1 esp on
parted -s "$OS_DISK" -- mkpart primary ext4 1024MiB 100%
# Detect partition names (handles nvme0n1p1 vs sda1)
if [[ "$OS_DISK" =~ [0-9]$ ]]; then
EFI_PART="${OS_DISK}p1"
ROOT_PART="${OS_DISK}p2"
else
EFI_PART="${OS_DISK}1"
ROOT_PART="${OS_DISK}2"
fi
mkfs.vfat -F 32 -n NIXOS_BOOT "$EFI_PART"
mkfs.ext4 -F -L NIXOS_ROOT "$ROOT_PART"
# Mount filesystems
mount "$ROOT_PART" /mnt
mkdir -p /mnt/boot
mount "$EFI_PART" /mnt/boot
# Generate hardware configuration
mkdir -p /mnt/etc/nixos
nixos-generate-config --root /mnt
# If creating pool now
if [ "$CREATE_POOL" = true ] && [ -n "${DISK_DEVICES:-}" ]; then
echo "Creating ZFS pool $POOL_NAME..."
zpool create -f -o ashift=12 -O compression=lz4 -O acltype=posixacl -O xattr=sa "$POOL_NAME" $DISK_DEVICES
zfs create "$POOL_NAME/media"
zfs create "$POOL_NAME/backup"
zfs create "$POOL_NAME/container"
fi
# Generate unique hostId
HOST_ID=$(head -c4 /dev/urandom | od -A none -t x4 | tr -d ' ')
# Write appliance flake and configuration
cat << NIXCONFIG > /mnt/etc/nixos/configuration.nix
{ config, pkgs, lib, ... }:
{
imports = [
./hardware-configuration.nix
];
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
boot.supportedFilesystems = [ "zfs" ];
networking.hostName = "$HOSTNAME";
networking.hostId = "$HOST_ID";
networking.useDHCP = lib.mkDefault $([ "$USE_DHCP" = "yes" ] && echo "true" || echo "false");
# Safe ZFS extra pools
boot.zfs.extraPools = [ "$POOL_NAME" $([ -n "$IMPORTED_POOL" ] && echo "\"$IMPORTED_POOL\"") ];
# Administrator user
users.users.$ADMIN_USER = {
isNormalUser = true;
extraGroups = [ "wheel" "docker" "video" "render" ];
openssh.authorizedKeys.keys = [ $([ -n "$ADMIN_SSH_KEY" ] && echo "\"$ADMIN_SSH_KEY\"") ];
};
security.sudo.wheelNeedsPassword = false;
# SSH Access
services.openssh.enable = true;
# State version
system.stateVersion = "24.11";
}
NIXCONFIG
echo -e "\n${GREEN}Beginning NixOS System Installation via nixos-install...${NC}"
nixos-install --no-root-passwd
echo -e "\n${BOLD}${GREEN}==============================================${NC}"
echo -e "${BOLD}${GREEN} NaxOS Installation Complete!${NC}"
echo -e "${BOLD}${GREEN} Remove the installation USB/ISO and reboot.${NC}"
echo -e "${BOLD}${GREEN} Access Dashboard on: http://$HOSTNAME.local${NC}"
echo -e "${BOLD}${GREEN}==============================================${NC}"
EOF
+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 ];
};
}
+28
View File
@@ -0,0 +1,28 @@
{ config, pkgs, lib, ... }:
{
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
boot.initrd.availableKernelModules = [
"xhci_pci"
"ahci"
"nvme"
"usb_storage"
"sd_mod"
"mpt3sas"
"virtio_pci"
"virtio_scsi"
"virtio_blk"
"virtio_net"
];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-intel" "kvm-amd" ];
boot.extraModulePackages = [ ];
# Dynamic DHCP on physical interfaces
networking.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
}
+82
View File
@@ -0,0 +1,82 @@
{ config, pkgs, lib, ... }:
{
# Core Appliance Settings
services.naxos.core = {
enable = true;
hostName = "naxos";
timeZone = "Europe/Berlin";
};
# Declarative ZFS Storage Engine
services.naxos.storage = {
enable = true;
arcMaxBytes = 4294967296; # 4GB default
autoScrub.enable = true;
autoTrim.enable = true;
autoSnapshot = {
enable = true;
hourly = 24;
daily = 7;
weekly = 4;
monthly = 12;
};
};
# Network File Shares
services.naxos.shares.samba = {
enable = true;
workgroup = "WORKGROUP";
serverString = "NaxOS Appliance";
netbiosName = "naxos";
};
services.naxos.shares.nfs = {
enable = true;
};
# Workload Runtime & App Engine
services.naxos.appEngine = {
enable = true;
defaultRuntime = "docker";
docker.enable = true;
gpuAcceleration.enable = true;
};
# Telemetry & Native Perses Analytics
services.naxos.monitoring = {
enable = true;
};
services.naxos.perses = {
enable = true;
port = 8080;
};
# Management API & Web Dashboard
services.naxos.api = {
enable = true;
port = 8088;
};
services.naxos.ui = {
enable = true;
port = 80;
sslPort = 443;
};
# GitOps Configuration
services.naxos.gitops = {
enable = true;
autoPushOnCommit = true;
};
# Automated Updates
services.naxos.selfUpdate = {
enable = true;
channel = "stable";
};
# Nix System State Version
system.stateVersion = "24.11";
}