Comments had drifted into multi-paragraph narrative (git commit lineage, debugging stories, restated code) in several hot spots (scripts/deploy, hermes-agent.nix, flake.nix, gitea.nix, headscale.nix). Trim every comment to its load-bearing "why" — gotchas, safety warnings, and non-obvious rationale survive verbatim in substance, just tightened to 1-2 sentences; historical narrative and anything already covered in CLAUDE.md is cut. No code/logic changed. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UJqEmY1y3AYX3JoX4Y6b21
163 lines
7.7 KiB
Nix
163 lines
7.7 KiB
Nix
{ config, pkgs, lib, ... }:
|
|
|
|
# ZimaBlade NAS host: hardware + disk + the services it runs.
|
|
{
|
|
imports = [
|
|
./hardware-configuration.nix
|
|
./disk-config.nix # disko: OS-disk partitions + filesystems
|
|
./secrets.nix # sops-nix: samba password, tailscale key, ...
|
|
../../common.nix # shared base: user / ssh / nix / firewall
|
|
../../services/network/samba.nix
|
|
../../services/network/avahi.nix
|
|
../../services/media/audiobookshelf.nix
|
|
../../services/containers.nix
|
|
../../services/network/caddy.nix
|
|
../../services/vpn/tailscale.nix
|
|
../../services/monitoring/node-exporter.nix
|
|
../../services/monitoring/victoriametrics.nix
|
|
../../services/media/jellyfin.nix
|
|
../../services/media/sabnzbd.nix
|
|
../../services/media/prowlarr.nix
|
|
../../services/media/sonarr.nix
|
|
../../services/media/radarr.nix
|
|
../../services/media/clonarr.nix
|
|
../../services/media/seerr.nix
|
|
../../services/media/immich.nix
|
|
../../services/dev/gitea.nix
|
|
../../services/dev/obsidian-livesync.nix
|
|
];
|
|
|
|
# sabnzbd's unrar dependency is unfree; scope the allowance to just that
|
|
# package rather than blanket-allowing unfree across the host.
|
|
nixpkgs.config.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [ "unrar" ];
|
|
|
|
# ---- Host identity ----
|
|
networking.hostName = "jupiter";
|
|
networking.networkmanager.enable = true;
|
|
users.users.darman.extraGroups = [ "docker" ]; # merges with common.nix
|
|
|
|
# ---- Boot ----
|
|
# systemd-boot for UEFI. If ZimaBlade boots legacy/BIOS, switch to grub.
|
|
boot.loader.systemd-boot.enable = true;
|
|
boot.loader.efi.canTouchEfiVariables = true;
|
|
# common.nix's default of 5 is still too many boot entries for a 29G eMMC — override down to 2.
|
|
boot.loader.systemd-boot.configurationLimit = lib.mkForce 2;
|
|
|
|
# A `switch` pins the old generation as a GC root until reboot; common.nix's weekly
|
|
# nix.gc is too slow for a 29G eMMC, so collect garbage on every boot instead.
|
|
systemd.services.gc-on-boot = {
|
|
description = "Full nix-collect-garbage on every boot";
|
|
wantedBy = [ "multi-user.target" ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
ExecStart = "${pkgs.nix}/bin/nix-collect-garbage -d";
|
|
};
|
|
};
|
|
|
|
# Root lives on the ZimaBlade eMMC (mmcblk0). nixos-generate-config runs in
|
|
# the RAM installer and does NOT detect these, so pin them here (merged with
|
|
# hardware-configuration.nix) or stage-1 can't mount root and the box panics.
|
|
boot.initrd.availableKernelModules = [ "mmc_block" "sdhci_pci" "sdhci_acpi" ];
|
|
|
|
# Warm reboot hangs at firmware reset on this board (cold power-cycle works).
|
|
# Force the PCI-chipset reset method. If a warm `reboot` still hangs, try the
|
|
# next value: acpi -> bios -> cold -> efi.
|
|
boot.kernelParams = [ "reboot=pci" ];
|
|
|
|
# ---- GPU (jellyfin hardware transcoding) ----
|
|
# Apollo Lake N3450 / HD Graphics 500 (Gen9). i915 binds on its own, but VAAPI needs
|
|
# the iHD userspace driver (Gen9; i965 is Gen8-only) or jellyfin-ffmpeg exits 251 on
|
|
# every transcode with no clearer error than "FFmpeg exited with code 251" in the log.
|
|
hardware.graphics = {
|
|
enable = true;
|
|
extraPackages = [ pkgs.intel-media-driver ];
|
|
};
|
|
# ⚠️ Use VAAPI, not QSV, in jellyfin's UI — QSV needs an MFX runtime not safely
|
|
# available for this Gen9 chip (only insecure/EOL options) and fails with exit 171.
|
|
# 4K HDR remuxes also can't be tone-mapped here (needs OpenCL or Gen11+); keep those
|
|
# as 1080p SDR or let them direct-play.
|
|
|
|
# ---- NAS data array ----
|
|
# Existing ext4 on mdadm RAID0 (sda+sdb, md0, 29.1T) — mounted, not formatted, kept
|
|
# out of disko. ⚠️ RAID0 has no redundancy: either disk failing loses ALL data.
|
|
boot.swraid.enable = true;
|
|
# Silences the "mdmon service will crash" eval warning — mdmon never actually runs
|
|
# here (native superblocks, not external-metadata) but the module warns regardless.
|
|
boot.swraid.mdadmConf = "MAILADDR root";
|
|
fileSystems."/mnt/data" = {
|
|
# fs UUID (stable) — the array may enumerate as /dev/md127, so avoid /dev/md0.
|
|
device = "/dev/disk/by-uuid/dadbff6f-652e-49b2-bfed-eb1308ab8b78";
|
|
fsType = "ext4";
|
|
options = [ "nofail" ]; # don't block boot if the array is degraded/absent
|
|
};
|
|
|
|
# `nofail` alone isn't enough — mounts layered on the array (prowlarr/seerr binds)
|
|
# are RequiredBy local-fs.target and can still trip Emergency Mode, which is a dead
|
|
# end here (root locked, no ssh). Boot as far as possible instead; the array-backed
|
|
# services carry RequiresMountsFor=/mnt/data so they still won't write to the eMMC.
|
|
systemd.enableEmergencyMode = false;
|
|
|
|
# ---- Heavy state moved off the eMMC ----
|
|
# A deploy holds two full closures (~9G each) on this 29G disk at once, so state
|
|
# that grows on its own can't live there — moved under /mnt/data/AppData like every
|
|
# other service's state. Settings below are jupiter-only; services/containers.nix
|
|
# stays engine/host-agnostic (mercury runs podman with no array).
|
|
|
|
# runroot stays on /run (per-boot tmpfs, doesn't grow); graphroot moves to the array
|
|
# since the gitea runner's CI images alone run several GB.
|
|
virtualisation.containers.storage.settings.storage = {
|
|
driver = "overlay";
|
|
graphroot = "/mnt/data/AppData/containers/storage";
|
|
runroot = "/run/containers/storage";
|
|
};
|
|
|
|
# immich's postgres cluster. Version-qualified path (matches upstream default) so a
|
|
# major bump gets a fresh dir instead of reusing the old cluster's files.
|
|
# ⚠️ Puts the DB in the same RAID0 failure domain as the photos it indexes —
|
|
# deliberate (the two are useless apart) but neither is backed up.
|
|
services.postgresql.dataDir =
|
|
"/mnt/data/AppData/postgresql/${config.services.postgresql.package.psqlSchema}";
|
|
|
|
# /mnt/data/AppData is drwx--x--- darman:users, so postgres needs the "users" group
|
|
# just to traverse into its dataDir (same reason immich needs it) — postgres itself
|
|
# refuses to start unless the cluster dir is 0700 or 0750.
|
|
users.users.postgres.extraGroups = [ "users" ];
|
|
|
|
# Neither path is under /var/lib, so no module creates it automatically — same
|
|
# reason immich needs its own mediaLocation tmpfiles rule.
|
|
systemd.tmpfiles.rules = [
|
|
"d /mnt/data/AppData/postgresql 0750 postgres postgres -"
|
|
"d /mnt/data/AppData/containers 0700 root root -"
|
|
];
|
|
|
|
# Without this, podman would recreate an empty store on the eMMC if the array mounts
|
|
# late or is absent, and the runner would re-pull every image.
|
|
# (podman-clonarr already sets this in services/media/clonarr.nix.)
|
|
systemd.services.podman.unitConfig.RequiresMountsFor = [ "/mnt/data" ];
|
|
systemd.services.gitea-runner-jupiter.unitConfig.RequiresMountsFor = [ "/mnt/data" ];
|
|
|
|
# ---- Caddy vhosts (LAN) ----
|
|
# Reached via pihole local-DNS names -> jupiter IP.
|
|
services.caddy.virtualHosts = {
|
|
"http://audiobookshelf.jupiter.sol".extraConfig = "reverse_proxy localhost:8000";
|
|
"http://jellyfin.jupiter.sol".extraConfig = "reverse_proxy localhost:8096";
|
|
"http://sabnzbd.jupiter.sol".extraConfig = "reverse_proxy localhost:8085";
|
|
"http://prowlarr.jupiter.sol".extraConfig = "reverse_proxy localhost:9696";
|
|
"http://sonarr.jupiter.sol".extraConfig = "reverse_proxy localhost:8989";
|
|
"http://radarr.jupiter.sol".extraConfig = "reverse_proxy localhost:7878";
|
|
"http://clonarr.jupiter.sol".extraConfig = "reverse_proxy localhost:6060";
|
|
"http://seerr.jupiter.sol".extraConfig = "reverse_proxy localhost:5055";
|
|
"http://gitea.jupiter.sol".extraConfig = "reverse_proxy localhost:3000";
|
|
# Immich uploads are large: raise the body limit off caddy's default and
|
|
# give slow phone uploads room before the proxy gives up.
|
|
"http://immich.jupiter.sol".extraConfig = ''
|
|
request_body {
|
|
max_size 50GB
|
|
}
|
|
reverse_proxy localhost:2283
|
|
'';
|
|
};
|
|
|
|
system.stateVersion = "26.05";
|
|
}
|