Nix-Vibe public snapshot (squashed history)

Current state of main at 0240060 feat(hp-laptop): install TeleportFling from its flake. History intentionally
collapsed to a single commit; this repo mirrors only the latest state.
This commit is contained in:
2026-09-19 13:53:39 +01:00
commit eb08cd4282
118 changed files with 18716 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
{ config
, pkgs
, lib
, ...
}:
{
options.services.backrest = {
enable = lib.mkEnableOption "Backrest web UI for restic backup";
port = lib.mkOption {
type = lib.types.port;
default = 9898;
description = "Port for the Backrest web UI";
};
host = lib.mkOption {
type = lib.types.str;
default = "127.0.0.1";
description = "Address to bind Backrest to";
};
dataDir = lib.mkOption {
type = lib.types.path;
default = "/var/lib/backrest";
description = "Directory for Backrest config and data";
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Open the firewall for the Backrest web UI";
};
};
config = lib.mkIf config.services.backrest.enable {
systemd.tmpfiles.rules = [
"d ${config.services.backrest.dataDir} 0700 backrest backrest -"
];
users.users.backrest = {
isSystemUser = true;
group = "backrest";
home = config.services.backrest.dataDir;
createHome = true;
};
users.groups.backrest = { };
systemd.services.backrest = {
description = "Backrest web UI for restic backup";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
serviceConfig = {
Type = "simple";
User = "backrest";
Group = "backrest";
ExecStart = ''
${pkgs.backrest}/bin/backrest \
-bind-address ${config.services.backrest.host}:${toString config.services.backrest.port} \
-data-dir ${config.services.backrest.dataDir} \
-restic-cmd ${pkgs.restic}/bin/restic
'';
Restart = "on-failure";
};
};
networking.firewall.allowedTCPPorts = lib.mkIf config.services.backrest.openFirewall [
config.services.backrest.port
];
};
}
+96
View File
@@ -0,0 +1,96 @@
{ config
, pkgs
, lib
, ...
}:
let
cfg = config.services.homepage;
in
{
options.services.homepage = {
enable = lib.mkEnableOption "Homepage dashboard (gethomepage.dev)";
port = lib.mkOption {
type = lib.types.port;
default = 8082;
description = "Port for the Homepage dashboard";
};
openFirewall = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Open the firewall for the Homepage dashboard";
};
allowedHosts = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [
"localhost"
"127.0.0.1"
];
description = "Host header values Homepage is allowed to respond to";
};
settings = lib.mkOption {
type = lib.types.anything;
default = { };
description = "Homepage settings (settings.yaml). See https://gethomepage.dev/configs/settings/";
};
services = lib.mkOption {
type = lib.types.anything;
default = [ ];
description = "Homepage services (services.yaml). See https://gethomepage.dev/configs/services/";
};
widgets = lib.mkOption {
type = lib.types.anything;
default = [ ];
description = "Homepage widgets (widgets.yaml). See https://gethomepage.dev/configs/info-widgets/";
};
bookmarks = lib.mkOption {
type = lib.types.anything;
default = [ ];
description = "Homepage bookmarks (bookmarks.yaml). See https://gethomepage.dev/configs/bookmarks/";
};
customCSS = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Custom CSS for Homepage";
};
customJS = lib.mkOption {
type = lib.types.lines;
default = "";
description = "Custom JavaScript for Homepage";
};
environmentFiles = lib.mkOption {
type = lib.types.listOf lib.types.path;
default = [ ];
description = "Environment files passed to the Homepage service (for API keys etc.)";
};
};
config = lib.mkIf cfg.enable {
services.homepage-dashboard = {
enable = true;
listenPort = cfg.port;
openFirewall = cfg.openFirewall;
allowedHosts = lib.concatStringsSep "," (
map (host: "${host}:${toString cfg.port}") cfg.allowedHosts
);
inherit (cfg)
settings
services
widgets
bookmarks
customCSS
customJS
environmentFiles
;
};
};
}
+55
View File
@@ -0,0 +1,55 @@
{ config
, pkgs
, lib
, ...
}:
{
options.services.immich-server = {
enable = lib.mkEnableOption "Immich photo management server";
port = lib.mkOption {
type = lib.types.port;
default = 2283;
description = "Port for Immich server";
};
mediaLocation = lib.mkOption {
type = lib.types.path;
default = "/data/immich";
description = "Directory for storing uploaded photos and videos";
};
};
config = lib.mkIf config.services.immich-server.enable {
services.immich = {
enable = true;
host = "0.0.0.0";
port = config.services.immich-server.port;
mediaLocation = config.services.immich-server.mediaLocation;
# Firewall is managed by the host (homeserver-1 restricts to tailscale).
openFirewall = false;
# Restrict hardware-acceleration access to the explicit NVIDIA device
# nodes needed for NVENC/CUDA transcoding (GTX 960). null would grant
# access to ALL devices; an explicit list is the secure alternative.
accelerationDevices = [
"/dev/nvidia0"
"/dev/nvidiactl"
"/dev/nvidia-modeset"
"/dev/nvidia-uvm"
"/dev/nvidia-uvm-tools"
];
};
# The upstream services.immich module creates a tmpfiles `e` rule that
# sets the media directory to 0700 on every rebuild/switch. Setting that
# mode clobbers the POSIX ACL mask (to `---`), which in turn nullifies the
# `user:backrest` read ACL that lets the backup service read this directory.
# Override it to keep the directory group-readable/traversable so the ACL
# mask stays effective (matches the rule just above).
systemd.tmpfiles.settings.immich.${config.services.immich-server.mediaLocation}.e.mode =
lib.mkForce "0770";
systemd.tmpfiles.rules = [
"d ${config.services.immich-server.mediaLocation} 0770 immich immich -"
];
};
}
+125
View File
@@ -0,0 +1,125 @@
{ config
, pkgs
, lib
, ...
}:
let
# Jellyfin SSO plugin built from upstream release zip.
# Not in nixpkgs, so we package it here for use in a bind mount.
ssoPlugin = pkgs.stdenvNoCC.mkDerivation {
pname = "jellyfin-plugin-sso";
version = "4.0.0.4";
src = pkgs.fetchzip {
url = "https://github.com/9p4/jellyfin-plugin-sso/releases/download/v4.0.0.4/sso-authentication_4.0.0.4.zip";
hash = "sha256-MJTyE6CeVLk7mlugauJ/F6bpi1kYwNtzNmQeH3+CFeQ=";
stripRoot = false;
};
dontBuild = true;
installPhase = ''
mkdir -p $out
cp -r ./* $out/
'';
};
# Plugin directory name expected by Jellyfin (version-specific).
pluginDirName = "SSO-Auth_4.0.0.4";
in
{
options.services.jellyfin-server = {
enable = lib.mkEnableOption "Jellyfin media server";
port = lib.mkOption {
type = lib.types.port;
default = 8096;
description = "Port for Jellyfin web interface";
};
mediaLocation = lib.mkOption {
type = lib.types.path;
default = "/data/jellyfin";
description = "Directory for storing media files";
};
};
config = lib.mkIf config.services.jellyfin-server.enable {
services.jellyfin = {
enable = true;
openFirewall = true;
group = "jellyfin";
# Hardware acceleration for NVIDIA NVENC transcoding (GTX 960 on homeserver-1).
# Requires the legacy_580 driver branch for Maxwell GPUs.
hardwareAcceleration = {
enable = true;
type = "nvenc";
device = "/dev/nvidia0";
};
# Force encoding config so NVENC settings are applied on every restart.
# Without this, changes made in the web UI persist, but NixOS settings are ignored.
forceEncodingConfig = true;
transcoding = {
# Enable hardware encoding for H.264 and H.265 (HEVC).
enableHardwareEncoding = true;
# Optional: enable tone mapping for HDR → SDR conversion.
enableToneMapping = false;
};
};
# Media dir: petere writes via Samba (forced to group jellyfin), Jellyfin
# reads as group jellyfin. Not world-writable.
# Also ensure the plugins directory exists for the SSO plugin bind mount.
systemd.tmpfiles.rules = [
"d ${config.services.jellyfin-server.mediaLocation} 0775 root jellyfin -"
"d /var/lib/jellyfin/plugins 0755 jellyfin jellyfin -"
];
# Samba share is authenticated and restricted to petere (the only writer).
# No guest access; the Samba password is managed via SOPS (see
# samba-petere-password secret and the samba-set-password service below).
services.samba = {
enable = true;
openFirewall = true;
settings = {
global = {
"server role" = "standalone server";
security = "user";
};
jellyfin = {
path = "${config.services.jellyfin-server.mediaLocation}";
browseable = "yes";
"read only" = "no";
"guest ok" = "no";
"valid users" = "petere";
"force group" = "jellyfin";
"force create mode" = "0664";
"force directory mode" = "0775";
};
};
};
# Install the SSO plugin by copying from the Nix store.
# Jellyfin needs to write plugin metadata (meta.json), so we can't use
# a read-only bind mount from /nix/store. This oneshot service is
# idempotent: it only copies if the plugin dir doesn't exist or differs.
systemd.services.jellyfin-install-plugin-sso = {
description = "Install Jellyfin SSO Auth plugin";
wantedBy = [ "multi-user.target" ];
before = [ "jellyfin.service" ];
serviceConfig.Type = "oneshot";
script = ''
PLUGIN_DIR="/var/lib/jellyfin/plugins/${pluginDirName}"
SOURCE="${ssoPlugin}"
# Only copy if the plugin doesn't exist or source changed
if [ ! -d "$PLUGIN_DIR" ] || ! diff -rq "$SOURCE" "$PLUGIN_DIR" >/dev/null 2>&1; then
rm -rf "$PLUGIN_DIR"
cp -r "$SOURCE" "$PLUGIN_DIR"
chown -R jellyfin:jellyfin "$PLUGIN_DIR"
chmod -R u+w "$PLUGIN_DIR"
fi
'';
};
systemd.services.jellyfin = {
after = [ "jellyfin-install-plugin-sso.service" ];
wants = [ "jellyfin-install-plugin-sso.service" ];
};
};
}
+49
View File
@@ -0,0 +1,49 @@
{ config
, pkgs
, lib
, ...
}:
{
options.services.ntfy-container = {
enable = lib.mkEnableOption "ntfy service";
port = lib.mkOption {
type = lib.types.port;
default = 8080;
description = "Host port to map to ntfy";
};
};
config = lib.mkIf config.services.ntfy-container.enable {
services.ntfy-sh = {
enable = true;
settings = {
base-url = "https://ntfy.edley.me";
# Bind to all interfaces - nginx proxy (in Docker container) needs to reach it.
# Security: firewall restricts access to nginx proxy machine only.
listen-http = ":${toString config.services.ntfy-container.port}";
cache-file = "/var/lib/ntfy-sh/cache.db";
auth-file = "/var/lib/ntfy-sh/user.db";
behind-proxy = true; # Explicitly enable proxy support (X-Forwarded-*)
# Security: Deny all access by default, require login
auth-default-access = "deny-all";
enable-signup = false;
enable-login = true;
# Require login for ALL web app actions - hides the public UI
# behind authentication so anonymous visitors can't even browse
# the web app.
require-login = true;
};
};
# Firewall: allow access only from nginx proxy machine (adjust IP as needed).
# Example: allow from 10.0.0.x (Tailscale IP of nginx proxy machine)
# networking.firewall.allowedTCPPorts = [ config.services.ntfy-container.port ];
# networking.firewall.interfaces.tailscale0.allowedTCPPorts = [ config.services.ntfy-container.port ];
# Ensure state directory exists (ntfy-sh service usually creates it, but good to ensure permissions if needed)
# The systemd service for ntfy-sh usually handles DynamicUser/StateDirectory or similar.
# We can trust the upstream module for basic setup.
};
}
+44
View File
@@ -0,0 +1,44 @@
{ config
, pkgs
, lib
, ...
}:
{
options.services.paperless-service = {
enable = lib.mkEnableOption "Paperless-ngx service";
passwordFile = lib.mkOption {
type = lib.types.path;
default = "/etc/nixos/paperless-password";
description = "Path to the file containing the admin password";
};
port = lib.mkOption {
type = lib.types.port;
default = 28981;
description = "Port to listen on";
};
};
config = lib.mkIf config.services.paperless-service.enable {
services.paperless = {
enable = true;
passwordFile = config.services.paperless-service.passwordFile;
# Listening on 0.0.0.0 but the firewall only opens the port on tailscale0,
# so Paperless is reachable over Tailscale only (not the LAN).
address = "0.0.0.0";
port = config.services.paperless-service.port;
settings = {
PAPERLESS_OCR_LANGUAGE = "eng";
# Set the URL to the Tailscale hostname
PAPERLESS_URL = "http://x1carbon:${toString config.services.paperless-service.port}";
# Allow requests from the Tailscale hostname
PAPERLESS_ALLOWED_HOSTS = "x1carbon,x1carbon.tailscale.net";
};
};
# Tailscale-only access: open the port on the tailscale0 interface only.
networking.firewall.interfaces.tailscale0.allowedTCPPorts = [
config.services.paperless-service.port
];
};
}