This commit is contained in:
Anish Lakhwara
2026-01-19 22:37:30 -08:00
parent 3b33575b2a
commit d0cde973e7
21 changed files with 818 additions and 243 deletions
+97
View File
@@ -0,0 +1,97 @@
# Helm Environment Overview
## Box NAS Server (`box` / `mossnet.lan`)
### Hardware
- NVMe boot drive (LUKS encrypted)
- 3x 4TB drives in ZFS RAIDZ1 pool (`tank`) - ~7.14TB usable
### ZFS Datasets
| Dataset | Mountpoint | Purpose |
|---------|------------|---------|
| `tank/data/media` | `/tank/media` | Media library (music, photos, tv, movies) |
| `tank/data/books` | `/tank/books` | Calibre library |
| `tank/data/podcasts` | `/tank/podcasts` | Podcast storage |
| `tank/data/new-music` | `/tank/new-music` | Incoming music from seedbox |
| `tank/data/backup` | `/tank/backup` | PostgreSQL backups |
| `tank/data/archive` | `/tank/archive` | Old data (memories, old-home-dirs, etc.) |
### Services
- **Immich** - Photo management (`/tank/media/photos`)
- **Gonic** - Music streaming (`/tank/media/music`)
- **Calibre-server/Calibre-web** - Ebook management (`/tank/books`)
- **Jellyfin** - Media streaming
- **Lidarr** - Music management (runs as `headphones:audio`)
- **Radicale** - CalDAV/CardDAV
- **Syncthing** - File sync
- **PostgreSQL** - Database
- **Taskserver** - Taskwarrior sync
---
## Repository Structure
```
helm/
├── flake.nix # Main flake - defines all hosts
├── hosts/
│ ├── box/
│ │ ├── default.nix # Box host config, imports profiles
│ │ ├── configuration.nix # Hardware/boot config
│ │ └── disko.nix # Disk/ZFS layout
│ ├── profiles/ # NixOS service profiles
│ │ ├── sync/music/ # get-music-sync service
│ │ ├── headphones/ # Lidarr config
│ │ ├── jellyfin/
│ │ ├── monitoring/
│ │ └── ...
├── home/
│ ├── dev/
│ │ └── default.nix # Home-manager config for box
│ └── profiles/
│ ├── beets/ # Beets music library config
│ ├── cli/
│ ├── nvim/
│ ├── git/
│ └── opencode/
├── secrets/ # Agenix encrypted secrets
└── modules/ # Custom NixOS modules
```
---
## Deployment
```bash
# Deploy to a host
deploy .#box
deploy .#curve
deploy .#helix
deploy .#lituus
# SSH access to box
ssh anish@mossnet.lan
```
---
## Key Users/Groups
| User | Group | Purpose |
|------|-------|---------|
| `anish` | `users`, `wheel`, `audio`, `video`, `docker` | Primary user |
| `headphones` | `audio` | Lidarr service |
| `gonic` | `audio` | Gonic music streaming |
| `immich` | `immich` | Photo management |
| `calibre-server` | `calibre-server` | Ebook server |
---
## Hosts
| Host | Description |
|------|-------------|
| `box` | NAS server (mossnet.lan) |
| `curve` | Workstation |
| `helix` | Workstation |
| `lituus` | VPS/Server |
Generated
+5 -5
View File
@@ -737,15 +737,15 @@
"treefmt-nix": "treefmt-nix_2" "treefmt-nix": "treefmt-nix_2"
}, },
"locked": { "locked": {
"lastModified": 1768359433, "lastModified": 1768434130,
"narHash": "sha256-e/6qI81VBJo0lAQsyUG+2jMsL0q3YLz88NZoZOCVFu8=", "narHash": "sha256-4rBBs7spDuimvUcL3egp2Zh94Lk8pf00VsjkOs59h7E=",
"owner": "Chickensoupwithrice", "owner": "numtide",
"repo": "llm-agents.nix", "repo": "llm-agents.nix",
"rev": "596bf03f14e9a54654473a1666b3b274bbc5939e", "rev": "d0ed3ef68a04b5bd127fecd405baf803eea29c29",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "Chickensoupwithrice", "owner": "numtide",
"repo": "llm-agents.nix", "repo": "llm-agents.nix",
"type": "github" "type": "github"
} }
+3 -3
View File
@@ -75,8 +75,7 @@
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
# LLM Agents (using fork until chainlink PR is merged) llm-agents.url = "github:numtide/llm-agents.nix";
llm-agents.url = "github:Chickensoupwithrice/llm-agents.nix";
# Others # Others
nur.url = "github:nix-community/NUR"; nur.url = "github:nix-community/NUR";
@@ -361,6 +360,7 @@
pkgs = nixpkgsFor.${system}; pkgs = nixpkgsFor.${system};
modules = [ modules = [
./hosts/box ./hosts/box
disko.nixosModules.disko
agenix.nixosModules.age agenix.nixosModules.age
self.nixosModules.backup self.nixosModules.backup
self.nixosModules.wireguard self.nixosModules.wireguard
@@ -368,7 +368,7 @@
self.nixosModules.gpodder2go self.nixosModules.gpodder2go
self.nixosModules.wallabag self.nixosModules.wallabag
self.nixosModules.ulogger-server self.nixosModules.ulogger-server
grasp.nixosModule # grasp.nixosModule # Disabled for initial install - private repo
home-manager.nixosModules.home-manager home-manager.nixosModules.home-manager
{ {
nix.registry.nixpkgs.flake = nixpkgs; nix.registry.nixpkgs.flake = nixpkgs;
+15 -3
View File
@@ -1,5 +1,17 @@
{ self, pkgs, inputs, ... }: { {
imports = self,
[ ../profiles/cli ../profiles/nvim ../profiles/direnv ../profiles/git ../profiles/opencode ]; pkgs,
inputs,
...
}:
{
imports = [
../profiles/cli
../profiles/nvim
../profiles/direnv
../profiles/git
../profiles/opencode
../profiles/beets
];
home.stateVersion = "22.05"; home.stateVersion = "22.05";
} }
+53
View File
@@ -0,0 +1,53 @@
{ pkgs, ... }:
{
programs.beets = {
enable = true;
package = pkgs.beets.override {
pluginOverrides = {
fetchart.enable = true;
embedart.enable = true;
lastgenre.enable = true;
duplicates.enable = true;
missing.enable = true;
};
};
settings = {
directory = "/tank/media/music";
library = "/home/anish/.local/share/beets/library.db";
import = {
move = true; # Move files from new-music to library
write = true; # Write tags to files
log = "/tank/new-music/beets-import.log";
incremental = true; # Skip already-imported directories
};
# Path format for organizing music
paths = {
default = "$albumartist/$album%aunique{}/$track $title";
singleton = "Non-Album/$artist/$title";
comp = "Compilations/$album%aunique{}/$track $title";
};
plugins = [
"fetchart"
"embedart"
"lastgenre"
"duplicates"
"missing"
];
fetchart = {
auto = true;
};
embedart = {
auto = true;
};
lastgenre = {
auto = true;
};
};
};
}
+52 -62
View File
@@ -1,79 +1,73 @@
# Edit this configuration file to define what should be installed on # Edit this configuration file to define what should be installed on
# your system. Help is available in the configuration.nix(5) man page # your system. Help is available in the configuration.nix(5) man page
# and in the NixOS manual (accessible by running nixos-help). # and in the NixOS manual (accessible by running 'nixos-help').
{ config, pkgs, ... }: { config, pkgs, ... }:
{ {
imports = imports = [
[ # Include the results of the hardware scan.
# Include the results of the hardware scan. ./hardware-configuration.nix
./hardware-configuration.nix ./disko.nix
]; ];
# No systemd emergency mode (can't reliably be accessed over SSH) # No systemd emergency mode (can't reliably be accessed over SSH)
systemd.enableEmergencyMode = false; systemd.enableEmergencyMode = false;
# Use the GRUB 2 boot loader. # ZFS requires a hostId
boot.loader.efi.canTouchEfiVariables = false; networking.hostId = "bb7d707a";
boot.loader.efi.efiSysMountPoint = "/boot/efi";
boot.loader.grub = { # Boot configuration for LUKS + ZFS
enable = true; boot.loader.efi.canTouchEfiVariables = true;
device = "nodev"; boot.loader.efi.efiSysMountPoint = "/boot";
efiSupport = true; boot.loader.systemd-boot.enable = true;
enableCryptodisk = true;
efiInstallAsRemovable = true; # ZFS support
boot.supportedFilesystems = [ "zfs" ];
boot.zfs = {
requestEncryptionCredentials = [ "tank" ]; # Load key for tank pool
forceImportRoot = false;
}; };
boot.initrd.secrets = { # ZFS services
"/keyfile0.bin" = /etc/secrets/initrd/keyfile0.bin; services.zfs = {
"/keyfile1.bin" = /etc/secrets/initrd/keyfile1.bin; autoScrub = {
}; enable = true;
interval = "weekly";
# Remote SSH unlock
boot.initrd.network.enable = true;
boot.initrd.network.ssh = {
enable = true;
port = 22;
authorizedKeys = [ "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDM0Zvei46x/yZl/IeBCq6+IYQQ0avulzVyBysF9cPigZMCybWRV7IEU+E3k9t6JrbdbdGfJkcZIWmsWDdKS8W8mBnZpVoT0ffLynu8JQ/TKdGm4Qv6bgUeKNrGsNv0ZPs2CDaGSLj0oJfRF7Ko10tcLP0vW+yujrh+y6TH/vVzJioaV4TGvtCUpn+wEQah9ROwPQLUUofsSWdnRsDJ/gp37zXWs4l5wyjSKtP3O9RZUP7kBekbSqEgSXiTk0oUQSVqIWl9NDiP6onk/gSOjXsR/JPqsSN/XI/c/yj6gyY0f51Ru2D7iBxuMJIJcWV+rU6coIj+ULcQWLzt/7TI8jq5AOOzI/ll4zbL24Eo84Rz+TP9tvMMhDZ0VaMN22AJ8qQEjc5P09tWKsX7Jg39XelyV1jHXncE4yvIE9F4RSCHzWCeKeXakizQNuzSaxTxIExRFYHjNW5bR6+3MTGwVrEIXU+qML+0yFTR86MT+tdY5AreAJQLwbog79O1NupeXJE= anish@curve " ];
hostKeys = [ "/etc/secrets/initrd/ssh_host_ed25519_key" ]; # create this file with `ssh-keygen -t ed25519 -N "" -f /etc/secrets/initrd/ssh_host_ed25519_key`
};
boot.initrd.availableKernelModules = [ "igc" "iwlwifi" ];
boot.initrd.luks.devices = {
"root" = {
#name = "root";
device = "/dev/disk/by-uuid/f37f3222-47d7-42d8-b400-363320a31853"; # UUID for /dev/nvme01np2
preLVM = true;
allowDiscards = true;
keyFile = "/keyfile0.bin";
}; };
autoSnapshot = {
enable = true;
frequent = 4; # 15-minute snapshots
hourly = 24;
daily = 7;
weekly = 4;
monthly = 12;
};
trim.enable = true;
}; };
# Data mount
# fileSystems."/data" = {
# device = "/dev/disk/by-uuid/3276a297-9ee4-4998-b262-1ed100366c06"; # UUID for /dev/mapper/crypted-data
# encrypted = {
# enable = true;
# label = "crypted-data";
# blkDev = "/dev/disk/by-uuid/8a317bf4-fe13-4334-a6df-5fe5a5048b5e"; # UUID for /dev/sda1
# keyFile = "/keyfile1.bin";
# };
# };
networking.interfaces.enp2s0 = { networking.interfaces.enp2s0 = {
ipv4.addresses = [{ ipv4.addresses = [
address = "192.168.1.240"; {
prefixLength = 24; address = "192.168.1.240";
}]; prefixLength = 24;
ipv6.addresses = [{ }
address = "fd7d:587a:4300:1::240"; ];
prefixLength = 64; ipv6.addresses = [
}]; {
ipv4.routes = [{ address = "192.168.1.0"; prefixLength = 24; via = "192.168.1.1"; }]; address = "fd7d:587a:4300:1::240";
prefixLength = 64;
}
];
ipv4.routes = [
{
address = "192.168.1.0";
prefixLength = 24;
via = "192.168.1.1";
}
];
useDHCP = false; useDHCP = false;
}; };
#networking.nameservers = [ "172.16.11.240" ];
networking.nameservers = [ "192.168.1.1" ]; networking.nameservers = [ "192.168.1.1" ];
networking.defaultGateway = { networking.defaultGateway = {
address = "192.168.1.1"; address = "192.168.1.1";
@@ -81,11 +75,8 @@
}; };
networking.hostName = "box"; # Define your hostname. networking.hostName = "box"; # Define your hostname.
# networking.wireless.enable = true; # Enables wireless support via wpa_supplicant.
# The global useDHCP flag is deprecated, therefore explicitly set to false here. # The global useDHCP flag is deprecated, therefore explicitly set to false here.
# Per-interface useDHCP will be mandatory in the future, so this generated config
# replicates the default behaviour.
networking.useDHCP = false; networking.useDHCP = false;
networking.interfaces.wlp3s0.useDHCP = true; networking.interfaces.wlp3s0.useDHCP = true;
@@ -103,4 +94,3 @@
system.stateVersion = "19.09"; # Did you read the comment? system.stateVersion = "19.09"; # Did you read the comment?
} }
+35 -29
View File
@@ -1,4 +1,5 @@
{ self, pkgs, ... }: { { self, pkgs, ... }:
{
imports = [ imports = [
./configuration.nix ./configuration.nix
../profiles/core ../profiles/core
@@ -6,7 +7,7 @@
../profiles/taskd ../profiles/taskd
../profiles/shaarli ../profiles/shaarli
../profiles/dns ../profiles/dns
# ../profiles/monitoring ../profiles/monitoring
../profiles/nfs ../profiles/nfs
../profiles/gonic ../profiles/gonic
../profiles/headphones ../profiles/headphones
@@ -19,64 +20,69 @@
../profiles/finance ../profiles/finance
../profiles/sync/website ../profiles/sync/website
../profiles/sync/music ../profiles/sync/music
../profiles/grasp # ../profiles/grasp # private repo - disabled
# ../profiles/archivebox # ../profiles/archivebox # requires insecure django - fix in flake.nix permittedInsecurePackages
# ../profiles/woodpecker-agent ../profiles/woodpecker-agent
# ../profiles/jellyfin ../profiles/jellyfin
../profiles/ulogger-server ../profiles/ulogger-server
../profiles/immich ../profiles/immich
../profiles/jacket ../profiles/jacket
../profiles/gpodder ../profiles/gpodder
../profiles/transmission ../profiles/transmission
../profiles/raven ../profiles/raven
#../profiles/postgres_upgrade_script # ../profiles/postgres_upgrade_script # one-time use
]; ];
# Backups # Backups
age.secrets.borg-password.file = "${self}/secrets/borg-password.age"; age.secrets.borg-password.file = "${self}/secrets/borg-password.age";
services.postgresqlBackup = { services.postgresqlBackup = {
enable = true; enable = true;
databases = [ "wallabag" "immich" "ulogger" ]; databases = [
location = "/var/backup/postgresql"; "wallabag"
"immich"
"ulogger"
];
location = "/tank/backup/postgresql";
}; };
mossnet.backup = { mossnet.backup = {
enable = true; enable = true;
name = "mossnet"; name = "mossnet";
paths = [ paths = [
"/var/lib/taskserver" # taskwarrior "/var/lib/taskserver" # taskwarrior
"/var/www/shaarli-config" # sharli "/var/www/shaarli-config" # shaarli
"/var/backup/postgresql" # wallabag "/tank/backup/postgresql" # postgresql backups
"/var/lib/radicale" # radicale "/var/lib/radicale" # radicale
"/home/anish/usr/drawing" # syncthing "/tank/syncthing/drawing" # syncthing
"/data/books" # calibre-web "/tank/books" # calibre-web
# "/home/anish/usr/nonfiction" # syncthing
"/home/anish/usr/finance" # beancount "/home/anish/usr/finance" # beancount
"/mnt/two/postgres" # sealight postgres backups TODO remove once moved to capsul "/tank/postgres" # postgres data
"/mnt/two/photos" "/tank/media/photos"
"/mnt/two/music" "/tank/media/music"
]; ];
# seafile
}; };
# opencode-manager ports # opencode-manager ports
networking.firewall = { networking.firewall = {
allowedTCPPorts = [ allowedTCPPorts = [
5003 # opencode-manager backend 5003 # opencode-manager backend
5173 # opencode-manager frontend 5173 # opencode-manager frontend
5551 # opencode server 5551 # opencode server
]; ];
allowedTCPPortRanges = [{ allowedTCPPortRanges = [
from = 7000; {
to = 9000; from = 7000;
}]; # ports for testing user changes to = 9000;
}
]; # ports for testing user changes
}; };
environment.systemPackages = with pkgs; [ lm_sensors ]; environment.systemPackages = with pkgs; [ lm_sensors ];
hardware.fancontrol = { # hardware.fancontrol = {
enable = false; # enable = false;
config = ''''; # config = '''';
}; # };
# Secrets
age.secrets.box-wg.file = "${self}/secrets/box-wg.age"; age.secrets.box-wg.file = "${self}/secrets/box-wg.age";
age.secrets.box-wg.owner = "anish"; age.secrets.box-wg.owner = "anish";
age.secrets.borg-key.file = "${self}/secrets/borg-key.age"; age.secrets.borg-key.file = "${self}/secrets/borg-key.age";
+238
View File
@@ -0,0 +1,238 @@
# Disko configuration for box NAS
# NVMe boot drive with LUKS + 3x 4TB ZFS RAIDZ1 pool (~8TB usable)
# Unified encryption: LUKS passphrase unlocks root, ZFS uses keyfile inside encrypted root
#
# NOTE: Using RAIDZ1 (3 drives) temporarily due to DOA drive. Can migrate to RAIDZ2
# with 4 drives later by creating a new pool and copying data.
#
# Installation steps:
#
# 1. Generate the ZFS keyfile and LUKS password:
# dd if=/dev/urandom of=/tmp/tank.key bs=32 count=1
# echo -n "your-luks-password" > /tmp/luks-password
#
# 2. Run disko-install:
# sudo nix run 'github:nix-community/disko/latest#disko-install' -- \
# --flake ~/helm#box \
# --disk nvme /dev/disk/by-id/nvme-CT500P310SSD8_2544543B87C2 \
# --disk zfs1 /dev/disk/by-id/ata-WDC_WD40EFPX-68C6CN0_WD-WX32D954A2J7 \
# --disk zfs2 /dev/disk/by-id/ata-WDC_WD40EFPX-68C6CN0_WD-WX32D95FVZVL \
# --disk zfs3 /dev/disk/by-id/ata-WDC_WD40EFPX-68C6CN0_WD-WX42D95M807R
#
# 3. Copy the keyfile and update keylocation:
# sudo mkdir -p /mnt/etc/zfs
# sudo cp /tmp/tank.key /mnt/etc/zfs/tank.key
# sudo chmod 000 /mnt/etc/zfs/tank.key
# sudo zfs set keylocation=file:///etc/zfs/tank.key tank
{
disko.devices = {
disk = {
# Boot drive - 500GB NVMe with LUKS encryption
nvme = {
type = "disk";
device = "/dev/disk/by-id/nvme-placeholder"; # Override with --disk nvme /dev/disk/by-id/...
content = {
type = "gpt";
partitions = {
ESP = {
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "umask=0077" ];
};
};
luks = {
size = "100%";
content = {
type = "luks";
name = "cryptroot";
settings = {
allowDiscards = true;
};
# Passphrase will be prompted during boot
passwordFile = "/tmp/luks-password"; # Only used during install, set this before running disko
content = {
type = "filesystem";
format = "ext4";
mountpoint = "/";
mountOptions = [ "noatime" ];
};
};
};
};
};
};
# ZFS pool drives - 3x 4TB in RAIDZ1
zfs1 = {
type = "disk";
device = "/dev/disk/by-id/placeholder-zfs1"; # Override with --disk zfs1 /dev/disk/by-id/...
content = {
type = "gpt";
partitions = {
zfs = {
size = "100%";
content = {
type = "zfs";
pool = "tank";
};
};
};
};
};
zfs2 = {
type = "disk";
device = "/dev/disk/by-id/placeholder-zfs2";
content = {
type = "gpt";
partitions = {
zfs = {
size = "100%";
content = {
type = "zfs";
pool = "tank";
};
};
};
};
};
zfs3 = {
type = "disk";
device = "/dev/disk/by-id/placeholder-zfs3";
content = {
type = "gpt";
partitions = {
zfs = {
size = "100%";
content = {
type = "zfs";
pool = "tank";
};
};
};
};
};
};
zpool = {
tank = {
type = "zpool";
mode = "raidz1";
options = {
ashift = "12";
cachefile = "none"; # Needed for disko
};
rootFsOptions = {
compression = "lz4";
atime = "off";
xattr = "sa";
acltype = "posixacl";
# ZFS native encryption
# During install: keyfile at /tmp/tank.key
# After install: install-box.sh copies to /etc/zfs/tank.key and updates keylocation
encryption = "aes-256-gcm";
keyformat = "raw";
keylocation = "file:///tmp/tank.key";
"com.sun:auto-snapshot" = "false";
};
# Don't mount the pool root directly
mountpoint = null;
datasets = {
# /nix is on the NVMe ext4 root, not on ZFS
# This simplifies boot dependencies - ZFS is purely for data storage
# Parent dataset for all data - inherits encryption
data = {
type = "zfs_fs";
options.mountpoint = "none";
};
# Media datasets
"data/media" = {
type = "zfs_fs";
options.mountpoint = "none";
};
"data/media/music" = {
type = "zfs_fs";
mountpoint = "/tank/media/music";
options.recordsize = "1M"; # Large files benefit from larger recordsize
};
"data/media/photos" = {
type = "zfs_fs";
mountpoint = "/tank/media/photos";
options.recordsize = "1M";
};
"data/media/movies" = {
type = "zfs_fs";
mountpoint = "/tank/media/movies";
options.recordsize = "1M";
};
"data/media/tv" = {
type = "zfs_fs";
mountpoint = "/tank/media/tv";
options.recordsize = "1M";
};
# Other data
"data/books" = {
type = "zfs_fs";
mountpoint = "/tank/books";
options."com.sun:auto-snapshot" = "true";
};
"data/podcasts" = {
type = "zfs_fs";
mountpoint = "/tank/podcasts";
};
"data/postgres" = {
type = "zfs_fs";
mountpoint = "/tank/postgres";
options = {
recordsize = "16K"; # Better for databases
"com.sun:auto-snapshot" = "true";
};
};
"data/syncthing" = {
type = "zfs_fs";
options.mountpoint = "none";
options."com.sun:auto-snapshot" = "true";
};
"data/syncthing/drawing" = {
type = "zfs_fs";
mountpoint = "/tank/syncthing/drawing";
};
"data/backup" = {
type = "zfs_fs";
mountpoint = "/tank/backup";
options."com.sun:auto-snapshot" = "true";
};
"data/ftp" = {
type = "zfs_fs";
mountpoint = "/tank/ftp";
};
"data/cache" = {
type = "zfs_fs";
mountpoint = "/tank/cache";
options."com.sun:auto-snapshot" = "false";
};
"data/playlists" = {
type = "zfs_fs";
mountpoint = "/tank/playlists";
};
"data/new-music" = {
type = "zfs_fs";
mountpoint = "/tank/new-music";
};
"data/archive" = {
type = "zfs_fs";
mountpoint = "/tank/archive";
options."com.sun:auto-snapshot" = "true";
};
};
};
};
};
}
+24 -56
View File
@@ -1,66 +1,34 @@
# Do not modify this file! It was generated by nixos-generate-config # Hardware configuration for box NAS
# and may be overwritten by future invocations. Please make changes # Filesystem mounts are handled by disko.nix
# to /etc/nixos/configuration.nix instead. {
{ config, lib, pkgs, modulesPath, ... }: config,
lib,
pkgs,
modulesPath,
...
}:
{ {
imports = imports = [
[ (modulesPath + "/installer/scan/not-detected.nix")
(modulesPath + "/installer/scan/not-detected.nix") ];
];
boot.initrd.availableKernelModules = [ "xhci_pci" "ahci" "thunderbolt" "uas" "usb_storage" "sd_mod" ]; boot.initrd.availableKernelModules = [
boot.initrd.kernelModules = [ "dm-snapshot" ]; "xhci_pci"
"ahci"
"nvme"
"thunderbolt"
"uas"
"usb_storage"
"sd_mod"
];
boot.initrd.kernelModules = [ ];
boot.kernelModules = [ "kvm-intel" ]; boot.kernelModules = [ "kvm-intel" ];
boot.extraModulePackages = [ ]; boot.extraModulePackages = [ ];
fileSystems."/" = # Filesystems are managed by disko - do not define them here
{
device = "/dev/disk/by-uuid/ade0752d-84d3-4e39-865b-9027ba2d5c67";
fsType = "ext4";
};
fileSystems."/boot/efi" = swapDevices = [ ];
{
device = "/dev/disk/by-uuid/1715-278E";
fsType = "vfat";
};
fileSystems."/mnt/one" =
{
device = "/dev/disk/by-uuid/0f857c6e-509d-436f-9e78-bc25f1b0d23b";
fsType = "ext4";
options = [
"noatime"
"nodiratime"
"nofail"
];
};
fileSystems."/mnt/two" =
{
device = "/dev/disk/by-uuid/5bc894bf-ed87-4c30-aab4-87e154e0cd08";
fsType = "ext4";
options = [
"noatime"
"nodiratime"
"nofail"
];
};
fileSystems."/mnt/three" =
{
device = "/dev/disk/by-uuid/0be3ded1-9c8b-40aa-94ca-dc2297d5988e";
fsType = "ext4";
options = [
"noatime"
"nodiratime"
"nofail"
];
};
swapDevices =
[{ device = "/dev/disk/by-uuid/b790abb4-ba5f-4476-8f09-b0fc575414aa"; }];
powerManagement.cpuFreqGovernor = lib.mkDefault "powersave"; powerManagement.cpuFreqGovernor = lib.mkDefault "powersave";
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
+55 -26
View File
@@ -1,4 +1,5 @@
{ self, pkgs, ... }: { { self, pkgs, ... }:
{
imports = [ imports = [
./configuration.nix ./configuration.nix
../users/anish ../users/anish
@@ -36,34 +37,45 @@
virtualisation.docker.enable = true; virtualisation.docker.enable = true;
virtualisation.docker.storageDriver = "btrfs"; virtualisation.docker.storageDriver = "btrfs";
environment.systemPackages = with pkgs; [ docker-compose via ]; environment.systemPackages = with pkgs; [
docker-compose
via
];
# Speed up boot by removing dependency on network # Speed up boot by removing dependency on network
systemd = { systemd = {
targets.network-online.wantedBy = targets.network-online.wantedBy = pkgs.lib.mkForce [ ]; # Normally ["multi-user.target"]
pkgs.lib.mkForce [ ]; # Normally ["multi-user.target"] services.NetworkManager-wait-online.wantedBy = pkgs.lib.mkForce [ ]; # Normally ["network-online.target"]
services.NetworkManager-wait-online.wantedBy =
pkgs.lib.mkForce [ ]; # Normally ["network-online.target"]
}; };
fileSystems."/mnt/ftp" = { fileSystems."/mnt/ftp" = {
device = "192.168.1.240:/home/ftp"; device = "192.168.1.240:/tank/ftp";
fsType = "nfs"; fsType = "nfs";
options = [ "x-systemd.automount" "noauto" "x-systemd.idle-timeout=600" ]; options = [
"x-systemd.automount"
"noauto"
"x-systemd.idle-timeout=600"
];
}; };
fileSystems."/mnt/tv" = { fileSystems."/mnt/tv" = {
device = "192.168.1.240:/mnt/three/tv"; device = "192.168.1.240:/tank/media/tv";
fsType = "nfs"; fsType = "nfs";
options = [ "x-systemd.automount" "noauto" "x-systemd.idle-timeout=600" ]; options = [
"x-systemd.automount"
"noauto"
"x-systemd.idle-timeout=600"
];
}; };
fileSystems."/mnt/movies" = { fileSystems."/mnt/movies" = {
device = "192.168.1.240:/mnt/three/movies"; device = "192.168.1.240:/tank/media/movies";
fsType = "nfs"; fsType = "nfs";
options = [ "x-systemd.automount" "noauto" "x-systemd.idle-timeout=600" ]; options = [
"x-systemd.automount"
"noauto"
"x-systemd.idle-timeout=600"
];
}; };
boot.supportedFilesystems = [ "ntfs" ]; boot.supportedFilesystems = [ "ntfs" ];
@@ -77,15 +89,23 @@
# lazy enable of ports necessary for KDE connect which is installed via cli home profile (for some reason?) # lazy enable of ports necessary for KDE connect which is installed via cli home profile (for some reason?)
networking.firewall = { networking.firewall = {
allowedTCPPorts = [ 22 4173 3000 ]; # allow ssh and vibekanban allowedTCPPorts = [
allowedTCPPortRanges = [{ 22
from = 1714; 4173
to = 1764; 3000
}]; ]; # allow ssh and vibekanban
allowedUDPPortRanges = [{ allowedTCPPortRanges = [
from = 1714; {
to = 1764; from = 1714;
}]; to = 1764;
}
];
allowedUDPPortRanges = [
{
from = 1714;
to = 1764;
}
];
}; };
age.secrets.curve-wg.file = "${self}/secrets/curve-wg.age"; age.secrets.curve-wg.file = "${self}/secrets/curve-wg.age";
@@ -105,7 +125,11 @@
mossnet.backup = { mossnet.backup = {
enable = true; enable = true;
name = "curve"; name = "curve";
paths = [ "/home/anish/usr" "/home/anish/.ssh" "/home/anish/.password-store/" ]; paths = [
"/home/anish/usr"
"/home/anish/.ssh"
"/home/anish/.password-store/"
];
}; };
# enable adb # enable adb
@@ -114,8 +138,13 @@
#virtualisation.docker.enable = true; #virtualisation.docker.enable = true;
boot.blacklistedKernelModules = [ "qcserial" ]; boot.blacklistedKernelModules = [ "qcserial" ];
# Used for packer Capsul # Used for packer Capsul
users.users.anish.extraGroups = users.users.anish.extraGroups = [
[ "adbusers" "wheel" "plugdev" "libvertd" "docker" ]; "adbusers"
"wheel"
"plugdev"
"libvertd"
"docker"
];
virtualisation.libvirtd.enable = true; virtualisation.libvirtd.enable = true;
hardware.keyboard.zsa.enable = true; hardware.keyboard.zsa.enable = true;
services.udev.extraRules = '' services.udev.extraRules = ''
+17 -6
View File
@@ -1,4 +1,10 @@
{ config, options, lib, pkgs, ... }: {
config,
options,
lib,
pkgs,
...
}:
with lib; with lib;
let let
# cfg = config.services.archivebox; # cfg = config.services.archivebox;
@@ -7,9 +13,8 @@ let
port = "8123"; port = "8123";
in in
{ {
nixpkgs.config.permittedInsecurePackages = [ # Note: permittedInsecurePackages must be set in flake.nix nixpkgsFor config
"python3.10-django-3.1.14" # if archivebox still requires python3.10-django-3.1.14
];
services.nginx.virtualHosts."archive.mossnet.lan" = { services.nginx.virtualHosts."archive.mossnet.lan" = {
enableACME = false; enableACME = false;
@@ -26,7 +31,10 @@ in
systemd.services.archivebox-install = { systemd.services.archivebox-install = {
description = "archivebox install service"; description = "archivebox install service";
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
path = with pkgs; [ coreutils archivebox ]; path = with pkgs; [
coreutils
archivebox
];
serviceConfig = { serviceConfig = {
User = user; User = user;
@@ -51,7 +59,10 @@ in
systemd.services.archivebox-server = { systemd.services.archivebox-server = {
description = "archivebox server service"; description = "archivebox server service";
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
path = with pkgs; [ coreutils archivebox ]; path = with pkgs; [
coreutils
archivebox
];
serviceConfig = { serviceConfig = {
User = user; User = user;
+2 -2
View File
@@ -8,7 +8,7 @@
user = "calibre-server"; user = "calibre-server";
group = "calibre-server"; group = "calibre-server";
options = { options = {
calibreLibrary = "/data/books"; calibreLibrary = "/tank/books";
enableBookUploading = true; enableBookUploading = true;
}; };
}; };
@@ -20,7 +20,7 @@
services.calibre-server = { services.calibre-server = {
enable = true; enable = true;
libraries = [ "/data/books" ]; libraries = [ "/tank/books" ];
# Bug in the module puts this in quotes in the systemd file # Bug in the module puts this in quotes in the systemd file
# user = calibre; # user = calibre;
# group = calibre; # group = calibre;
+10 -5
View File
@@ -1,12 +1,17 @@
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
...
}:
{ {
environment.systemPackages = [ pkgs.ffmpeg ]; environment.systemPackages = [ pkgs.ffmpeg ];
mossnet.gonic.enable = true; mossnet.gonic.enable = true;
mossnet.gonic.settings = '' mossnet.gonic.settings = ''
music-path /mnt/two/music/ music-path /tank/media/music/
podcast-path /data/podcasts podcast-path /tank/podcasts
cache-path /data/cache cache-path /tank/cache
playlists-path /data/playlists playlists-path /tank/playlists
''; '';
mossnet.gonic.user = "gonic"; mossnet.gonic.user = "gonic";
mossnet.gonic.group = "audio"; mossnet.gonic.group = "audio";
+7 -2
View File
@@ -1,4 +1,9 @@
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
...
}:
{ {
services.headphones = { services.headphones = {
enable = true; enable = true;
@@ -6,7 +11,7 @@
port = 8181; port = 8181;
user = "headphones"; user = "headphones";
group = "audio"; group = "audio";
dataDir = "/data/music"; dataDir = "/tank/media/music";
}; };
services.nginx.virtualHosts."headphones.mossnet.lan" = { services.nginx.virtualHosts."headphones.mossnet.lan" = {
enableACME = false; enableACME = false;
+1 -1
View File
@@ -8,7 +8,7 @@
}; };
host = "0.0.0.0"; host = "0.0.0.0";
port = 8567; port = 8567;
mediaLocation = "/mnt/two/photos"; mediaLocation = "/tank/media/photos";
openFirewall = true; openFirewall = true;
settings.server.externalDomain = "https://photos.sealight.xyz"; settings.server.externalDomain = "https://photos.sealight.xyz";
}; };
+23 -8
View File
@@ -1,10 +1,13 @@
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
...
}:
{ {
# Enable Hardware Acceleration for transcoding # Enable Hardware Acceleration for transcoding
nixpkgs.config.packageOverrides = pkgs: { # Note: vaapiIntel override with enableHybridCodec should be in flake.nix overlay if needed
vaapiIntel = pkgs.vaapiIntel.override { enableHybridCodec = true; }; hardware.graphics = {
};
hardware.opengl = {
enable = true; enable = true;
extraPackages = with pkgs; [ extraPackages = with pkgs; [
intel-media-driver intel-media-driver
@@ -18,10 +21,22 @@
enable = true; enable = true;
user = "jellyfin"; user = "jellyfin";
group = "video"; group = "video";
openFirewall = true; # only for defaults openFirewall = true; # only for defaults (8096)
}; };
networking.firewall.allowedTCPPorts = [ 8181 ];
users.users.jellyfin = { users.users.jellyfin = {
extraGroups = [ "video" "audio" ]; extraGroups = [
"video"
"audio"
];
};
services.nginx = {
enable = true;
virtualHosts = {
"jellyfin.mossnet.lan" = {
forceSSL = false;
enableACME = false;
locations."/".proxyPass = "http://localhost:8096/";
};
};
}; };
} }
+29 -17
View File
@@ -1,4 +1,10 @@
{ self, config, pkgs, ... }: { {
self,
config,
pkgs,
...
}:
{
age.secrets.nullhex-smtp.file = "${self}/secrets/nullhex-smtp.age"; age.secrets.nullhex-smtp.file = "${self}/secrets/nullhex-smtp.age";
age.secrets.nullhex-smtp.owner = "grafana"; age.secrets.nullhex-smtp.owner = "grafana";
@@ -33,21 +39,23 @@
}; };
}; };
services.postgresql = { services.postgresql = {
ensureUsers = [{ ensureUsers = [
{
name = "grafana"; name = "grafana";
# TODO this is deprecated }
# Need to translate this to ];
# systemd.services.postgresql.postStart };
# or initialScript
ensurePermissions = { # Grant grafana user read access to databases for monitoring
"ALL TABLES IN SCHEMA public" = "SELECT"; systemd.services.postgresql.postStart = pkgs.lib.mkAfter ''
"DATABASE wallabag" = "CONNECT"; $PSQL -tAc "GRANT CONNECT ON DATABASE wallabag TO grafana" 2>/dev/null || true
"DATABASE ulogger" = "CONNECT"; $PSQL -tAc "GRANT CONNECT ON DATABASE ulogger TO grafana" 2>/dev/null || true
"DATABASE photoprism" = "CONNECT"; $PSQL -tAc "GRANT CONNECT ON DATABASE photoprism TO grafana" 2>/dev/null || true
}; $PSQL -d wallabag -tAc "GRANT SELECT ON ALL TABLES IN SCHEMA public TO grafana" 2>/dev/null || true
}]; $PSQL -d ulogger -tAc "GRANT SELECT ON ALL TABLES IN SCHEMA public TO grafana" 2>/dev/null || true
}; $PSQL -d photoprism -tAc "GRANT SELECT ON ALL TABLES IN SCHEMA public TO grafana" 2>/dev/null || true
'';
services.prometheus = { services.prometheus = {
enable = true; enable = true;
@@ -66,11 +74,15 @@
scrapeConfigs = [ scrapeConfigs = [
{ {
job_name = "box"; job_name = "box";
static_configs = [{ targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.node.port}" ]; }]; static_configs = [
{ targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.node.port}" ]; }
];
} }
{ {
job_name = "dns"; job_name = "dns";
static_configs = [{ targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.dnsmasq.port}" ]; }]; static_configs = [
{ targets = [ "127.0.0.1:${toString config.services.prometheus.exporters.dnsmasq.port}" ]; }
];
} }
]; ];
}; };
+21 -6
View File
@@ -12,16 +12,31 @@
statdPort = 4000; statdPort = 4000;
extraNfsdConfig = ''''; extraNfsdConfig = '''';
exports = '' exports = ''
/home/ftp 192.168.1.0/24(rw) /tank/ftp 192.168.1.0/24(rw)
/mnt/one 192.168.1.0/24(rw) /tank/media/music 192.168.1.0/24(rw,async,no_subtree_check) 10.0.69.0/24(rw,async,no_subtree_check)
/mnt/two 192.168.1.0/24(rw,async,no_subtree_check) 10.0.69.0/24(rw,async,no_subtree_check) /tank/media/photos 192.168.1.0/24(rw,async,no_subtree_check) 10.0.69.0/24(rw,async,no_subtree_check)
/mnt/three 192.168.1.0/24(rw) /tank/media/movies 192.168.1.0/24(rw)
/tank/media/tv 192.168.1.0/24(rw)
''; '';
}; };
networking.firewall = { networking.firewall = {
allowedTCPPorts = [ 111 2049 4000 4001 4002 20048 ]; allowedTCPPorts = [
allowedUDPPorts = [ 111 2049 4000 4001 4002 20048 ]; 111
2049
4000
4001
4002
20048
];
allowedUDPPorts = [
111
2049
4000
4001
4002
20048
];
}; };
#systemd.services.create-mount-dir = { #systemd.services.create-mount-dir = {
+4 -4
View File
@@ -4,9 +4,9 @@ set -euo pipefail
REMOTE_HOST="aynish@talos.feralhosting.com" REMOTE_HOST="aynish@talos.feralhosting.com"
REMOTE_PATH="private/transmission/data/" REMOTE_PATH="private/transmission/data/"
LOCAL_PATH="/mnt/two/incoming" LOCAL_PATH="/tank/new-music"
TRACKING_FILE="/mnt/two/incoming/.downloaded_albums" TRACKING_FILE="/tank/new-music/.downloaded_albums"
LOG_FILE="/mnt/two/incoming/download-log" LOG_FILE="/tank/new-music/download-log"
# Create tracking file if it doesn't exist # Create tracking file if it doesn't exist
touch "$TRACKING_FILE" touch "$TRACKING_FILE"
@@ -47,7 +47,7 @@ while IFS= read -r album; do
echo "$(date): Importing $album to beets..." >>"$LOG_FILE" echo "$(date): Importing $album to beets..." >>"$LOG_FILE"
# Set umask to allow group read/write access # Set umask to allow group read/write access
umask 002 umask 002
if beet -p fetchart import -m -l /home/anish/music.log -q -g "$LOCAL_PATH/$album"; then if beet import -q "$LOCAL_PATH/$album"; then
echo "$(date): Successfully imported $album to beets" >>"$LOG_FILE" echo "$(date): Successfully imported $album to beets" >>"$LOG_FILE"
else else
echo "$(date): Failed to import $album to beets" >>"$LOG_FILE" echo "$(date): Failed to import $album to beets" >>"$LOG_FILE"
+3 -3
View File
@@ -11,12 +11,12 @@
# Normally, I would write this into the homedir with home-manager # Normally, I would write this into the homedir with home-manager
# And explictly set the dir to be the output of the home-manager location # And explictly set the dir to be the output of the home-manager location
script-torrent-done-filename = pkgs.writeShellScript "beet-import.sh" '' script-torrent-done-filename = pkgs.writeShellScript "beet-import.sh" ''
#!/usr/bin/env bash #!/usr/bin/env bash
beet -p fetchart import -l /home/anish/music.log -q -g "$TR_TORRENT_DIR" beet -p fetchart import -l /home/anish/music.log -q -g "$TR_TORRENT_DIR"
''; '';
rpc-url = "/transmission/rpc/"; rpc-url = "/transmission/rpc/";
download-dir = "/mnt/two/new-music"; download-dir = "/tank/new-music";
}; };
}; };
services.nginx.virtualHosts."transmission.mossnet.lan" = { services.nginx.virtualHosts."transmission.mossnet.lan" = {
Executable
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env bash
set -euo pipefail
# Install script for box NAS
# Run this from the NixOS installer after rsync'ing the helm repo
#
# Prerequisites:
# - Boot NixOS installer
# - Enable SSH: passwd && sudo systemctl start sshd
# - rsync helm repo: rsync -avz --exclude='.git' /path/to/helm nixos@<IP>:~/
#
# Usage:
# cd ~/helm
# ./install-box.sh
# Configuration
FLAKE="$HOME/helm#box"
NVME="/dev/disk/by-id/nvme-CT500P310SSD8_2544543B87C2"
# ZFS drives - update these if drives change
ZFS1="/dev/disk/by-id/ata-WDC_WD40EFPX-68C6CN0_WD-WX32D954A2J7"
ZFS2="/dev/disk/by-id/ata-WDC_WD40EFPX-68C6CN0_WD-WX32D95FVZVL"
ZFS3="/dev/disk/by-id/ata-WDC_WD40EFPX-68C6CN0_WD-WX42D95M807R"
echo "=== Box NAS Installation ==="
echo ""
echo "This will install NixOS with:"
echo " - NVMe boot drive: $NVME"
echo " - ZFS RAIDZ1 pool with 3x 4TB drives (~8TB usable)"
echo ""
# Verify drives exist
echo "Verifying drives..."
for disk in "$NVME" "$ZFS1" "$ZFS2" "$ZFS3"; do
if [[ ! -e "$disk" ]]; then
echo "ERROR: Disk not found: $disk"
echo "Available disks:"
ls -la /dev/disk/by-id/ | grep -E '(nvme|ata)' | grep -v part
exit 1
fi
done
echo "All drives found."
echo ""
# Generate ZFS keyfile
echo "Generating ZFS keyfile..."
dd if=/dev/urandom of=/tmp/tank.key bs=32 count=1 2>/dev/null
echo "ZFS keyfile created at /tmp/tank.key"
echo ""
# Get LUKS password
echo "Enter LUKS password for boot drive encryption:"
read -s LUKS_PASSWORD
echo ""
echo "Confirm LUKS password:"
read -s LUKS_PASSWORD_CONFIRM
echo ""
if [[ "$LUKS_PASSWORD" != "$LUKS_PASSWORD_CONFIRM" ]]; then
echo "ERROR: Passwords do not match"
exit 1
fi
echo -n "$LUKS_PASSWORD" > /tmp/luks-password
echo "LUKS password saved."
echo ""
# Confirm before proceeding
echo "WARNING: This will DESTROY all data on the following drives:"
echo " - $NVME"
echo " - $ZFS1"
echo " - $ZFS2"
echo " - $ZFS3"
echo ""
read -p "Type 'yes' to continue: " CONFIRM
if [[ "$CONFIRM" != "yes" ]]; then
echo "Aborted."
exit 1
fi
echo ""
echo "Running disko-install..."
sudo nix \
--extra-experimental-features nix-command \
--extra-experimental-features flakes \
run 'github:nix-community/disko/latest#disko-install' -- \
--flake "$FLAKE" \
--disk nvme "$NVME" \
--disk zfs1 "$ZFS1" \
--disk zfs2 "$ZFS2" \
--disk zfs3 "$ZFS3"
echo ""
echo "Copying ZFS keyfile to installed system..."
# disko-install mounts the root filesystem at /mnt
if [[ ! -d /mnt/etc ]]; then
echo "ERROR: /mnt/etc does not exist. Is the root filesystem mounted?"
exit 1
fi
sudo mkdir -p /mnt/etc/zfs
sudo cp /tmp/tank.key /mnt/etc/zfs/tank.key
sudo chmod 000 /mnt/etc/zfs/tank.key
echo "Updating ZFS keylocation to permanent path..."
# Update keylocation so ZFS looks for the key in the installed system
sudo zfs set keylocation=file:///etc/zfs/tank.key tank
echo ""
echo "Cleaning up..."
rm -f /tmp/luks-password /tmp/tank.key
echo ""
echo "=== Installation complete! ==="
echo ""
echo "Next steps:"
echo " 1. Reboot: sudo reboot"
echo " 2. Enter LUKS password at boot prompt"
echo " 3. SSH to box at 192.168.1.240"
echo ""