{ config, pkgs, ... }: { ######################################## # Samba File Sharing ######################################## services.samba = { enable = true; openFirewall = true; # Enable nmbd for NetBIOS name resolution and browsing (legacy Windows/macOS) nmbd.enable = true; # Enable WSD discovery (Windows) and mDNS/NetBIOS advertisement improvements via wsdd + Avahi. # "smb encrypt" set to desired to avoid discovery issues with some clients while still allowing encryption. settings = { global = { workgroup = "WORKGROUP"; "server string" = "NixFiles"; "netbios name" = "fileserver"; security = "user"; "map to guest" = "bad user"; "unix extensions" = "no"; # macOS optimizations "vfs objects" = "catia fruit streams_xattr"; "fruit:aapl" = "yes"; "fruit:metadata" = "stream"; "fruit:resource" = "stream"; "fruit:model" = "MacSamba"; "fruit:advertise_fullsync" = "yes"; "ea support" = "yes"; # Time Machine support "fruit:time machine" = "yes"; "fruit:time machine max size" = "500G"; # Prefer encryption but do not require it for basic browsing/guest visibility "smb encrypt" = "desired"; # Better macOS discovery "min protocol" = "SMB2"; "max protocol" = "SMB3"; "server role" = "standalone server"; # Allow Apple extended attributes & full sync advertisement already configured above. }; daten = { path = "/data/daten"; browseable = "yes"; "read only" = "no"; "valid users" = "danlin"; "fruit:time machine" = "no"; }; daten-share = { path = "/data/daten/share"; browseable = "yes"; "read only" = "no"; "guest ok" = "yes"; # Gastzugriff erlaubt public = "yes"; # Alias für guest ok # Removed "valid users" to allow true guest access; danlin can still write via group permissions "fruit:time machine" = "no"; }; timemachine = { path = "/data/backup/timemachine"; browseable = "yes"; "read only" = "no"; "valid users" = "danlin"; "fruit:time machine" = "yes"; "fruit:time machine max size" = "500G"; }; }; }; ######################################## # Network Discovery Services (Avahi + WSD) ######################################## # Avahi for macOS Finder discovery (mDNS) and general _smb._tcp advertising services.avahi = { enable = true; nssmdns4 = true; # Provide .local resolution openFirewall = true; publish = { enable = true; userServices = true; }; # Explicit SMB service advertisement for macOS Finder extraServiceFiles = { smb = '' %h _smb._tcp 445 _device-info._tcp 0 model=TimeCapsule8,119 _adisk._tcp 9 sys=waMa=0,adVF=0x100 dk0=adVN=timemachine,adVF=0x82 ''; }; }; # Windows network neighborhood discovery via Web Services for Devices (WSD) # NixOS does not provide a `services.wsdd` option in this release, so define a systemd unit. systemd.services.wsdd = { description = "Web Services Discovery daemon for Samba (wsdd)"; wantedBy = [ "multi-user.target" ]; after = [ "network-online.target" "samba.service" ]; wants = [ "network-online.target" ]; serviceConfig = { Type = "simple"; ExecStart = "${pkgs.wsdd}/bin/wsdd --hostname fileserver --workgroup WORKGROUP --interface ens18"; Restart = "on-failure"; RestartSec = 5; }; }; ######################################## # Directory Structure ######################################## systemd.tmpfiles.rules = [ "d /data 0775 root root -" "d /data/daten 0775 danlin users -" "d /data/daten/share 0775 danlin users -" # Gitea (runs as git user inside container, uid 1000) "d /data/gitea 0755 root root -" # DJ-Struktur "d /data/daten/DJing 0775 danlin users -" "d /data/daten/DJing/Music 0775 danlin users -" "d /data/daten/DJing/Data 0775 danlin users -" "d /data/daten/DJing/Inbox 0775 danlin users -" # Time Machine backup "d /data/backup 0775 root root -" "d /data/backup/timemachine 0775 danlin users -" # FileBrowser config — muss dem Container-User (uid 1000) gehören, # das Image läuft non-root als filebrowser:1000. "d /data/filebrowser 0755 danlin users -" # Das Image bringt eine config.yaml mit, die aber vom Bind-Mount auf # /home/filebrowser/data verdeckt wird — ohne sie startet es nicht. # "C" legt sie nur an, wenn sie fehlt; Änderungen bleiben erhalten. "C /data/filebrowser/config.yaml 0644 danlin users - ${pkgs.writeText "filebrowser-config.yaml" '' server: port: 80 baseURL: "/" logging: - levels: "info|warning|error" sources: - path: "/srv" ''}" # Secrets "d /data/secrets 0700 root root -" ]; ######################################## # Virtualization & Containers ######################################## # Virtualization virtualisation.podman.enable = true; virtualisation.oci-containers.backend = "podman"; virtualisation.oci-containers.containers.filebrowser = { image = "gtstef/filebrowser:stable"; autoStart = true; ports = [ "8080:80" ]; volumes = [ "/data:/srv" # Das Image legt DB + config.yaml unter /home/filebrowser/data ab # (FILEBROWSER_DATABASE/-CONFIG). Ohne diesen Mount lebt die Datenbank # nur im Container und ist nach jedem Neustart weg. "/data/filebrowser:/home/filebrowser/data" ]; environment = { TZ = "Europe/Berlin"; }; environmentFiles = [ "/data/secrets/filebrowser.env" ]; # Läuft als non-root (uid 1000) und dürfte Port 80 sonst nicht binden. extraOptions = [ "--sysctl" "net.ipv4.ip_unprivileged_port_start=0" ]; }; virtualisation.oci-containers.containers.gitea = { image = "gitea/gitea:latest"; autoStart = true; ports = [ "3000:3000" "2222:2222" ]; volumes = [ "/data/gitea:/data" ]; environment = { USER_UID = "1000"; USER_GID = "1000"; TZ = "Europe/Berlin"; GITEA__server__DOMAIN = "git.home.lindenfelser.de"; GITEA__server__ROOT_URL = "https://git.home.lindenfelser.de/"; GITEA__server__SSH_DOMAIN = "git.home.lindenfelser.de"; GITEA__server__SSH_PORT = "2222"; GITEA__server__SSH_LISTEN_PORT = "2222"; GITEA__server__START_SSH_SERVER = "true"; # Actions (CI) aktivieren — Voraussetzung für den act_runner unten. GITEA__actions__ENABLED = "true"; }; }; ######################################## # Gitea Actions Runner ######################################## # Job-Container laufen über das bereits vorhandene Podman (das Modul erkennt # das automatisch: DOCKER_HOST -> /run/podman/podman.sock, Gruppe "podman"). services.gitea-actions-runner = { package = pkgs.gitea-actions-runner; instances.fileserver = { enable = true; name = "fileserver"; # Interne LAN-URL — vom Host UND aus den Docker-Job-Containern erreichbar # (localhost/127.0.0.1 würde im Job-Container auf den Container selbst zeigen). url = "http://10.202.82.6:3000"; # Registration-Token: in Gitea unter Site Admin → Actions → Runners → # "Create new Runner" erzeugen und in diese Datei schreiben (0600 root). tokenFile = "/data/secrets/gitea-runner-token"; labels = [ "ubuntu-latest:docker://node:20-bookworm" "ubuntu-22.04:docker://node:20-bookworm" "alpine:docker://alpine:3.19" ]; }; }; # Reaper für verwaiste Job-Container. Wird der Runner mitten in einem Job # gestoppt (Reboot, autoUpgrade, Gitea-Neustart), läuft dessen Container mit # "sleep 10800" bis zu 3h weiter und wird nie abgeräumt. Dieser Timer entfernt # solche Container — aber NUR, wenn Gitea den zugehörigen Task als beendet # führt (Status 1/2/3/4). Laufende (6) oder unbekannte Tasks bleiben unberührt. systemd.services.gitea-runner-reaper = { description = "Reap orphaned Gitea Actions job containers"; after = [ "podman.service" ]; serviceConfig = { Type = "oneshot"; User = "root"; ExecStart = pkgs.writeShellScript "gitea-runner-reaper" '' set -uo pipefail PODMAN=${pkgs.podman}/bin/podman # Ohne laufenden Gitea-Container keine DB-Abfrage möglich -> nichts tun. if ! $PODMAN container exists gitea; then echo "gitea container not running — skipping reap" exit 0 fi $PODMAN ps --filter name=GITEA-ACTIONS-TASK- --format '{{.Names}}' | while read -r c; do [ -n "$c" ] || continue tid=$(printf '%s' "$c" | sed -n 's/^GITEA-ACTIONS-TASK-\([0-9]\+\)_.*/\1/p') [ -n "$tid" ] || continue # immutable=1: lock-freier read-only Zugriff auf die (von Gitea offene) DB. st=$($PODMAN exec gitea sqlite3 "file:/data/gitea/gitea.db?immutable=1" \ "SELECT status FROM action_task WHERE id=$tid;" 2>/dev/null) # Status: 1=success 2=failure 3=cancelled 4=skipped 5=waiting 6=running case "$st" in 1|2|3|4) echo "reaping orphan $c (task $tid finished, status=$st)" $PODMAN rm -f "$c" >/dev/null 2>&1 || true ;; *) : ;; # 6/5/leer/unbekannt -> in Ruhe lassen esac done # Übrig gebliebene Actions-Netzwerke/Volumes und dangling Images aufräumen # (prune entfernt nur, was an KEINEN laufenden Container gebunden ist). $PODMAN network prune -f >/dev/null 2>&1 || true $PODMAN volume prune -f >/dev/null 2>&1 || true $PODMAN image prune -f >/dev/null 2>&1 || true ''; }; }; systemd.timers.gitea-runner-reaper = { wantedBy = [ "timers.target" ]; timerConfig = { OnCalendar = "*:0/15"; Persistent = true; }; }; ######################################## # Backup Services ######################################## systemd.services.rclone-backup = { description = "Backup /data to Google Drive via rclone"; serviceConfig = { Type = "oneshot"; ExecStart = pkgs.writeShellScript "rclone-backup" '' set -uo pipefail # Laufende SQLite-DBs lassen sich nicht als Datei kopieren — sie ändern # sich während des Transfers (md5-Fehler). Daher vorab ein konsistentes # Hot-Backup (.backup) in einen Snapshot-Ordner ziehen, der mitgesichert # wird; die Live-Dateien selbst schließen wir unten aus. SNAP=/data/db-snapshots mkdir -p "$SNAP" for db in \ /data/gitea/gitea/gitea.db \ /data/filebrowser/database.db \ /data/daten/DJing/Data/MusicLibrary.db; do [ -f "$db" ] || continue out="$SNAP/$(echo "$db" | sed 's#^/data/##; s#/#_#g')" # Echtes SQLite -> konsistentes .backup. Andernfalls (filebrowser # nutzt z.B. BoltDB) oder bei Sperre auf eine Rohkopie zurückfallen. if ${pkgs.sqlite}/bin/sqlite3 "$db" ".timeout 15000" ".backup '$out'" 2>/dev/null && [ -s "$out" ]; then : else cp -f "$db" "$out" || echo "WARN: snapshot of $db failed" >&2 fi done ${pkgs.rclone}/bin/rclone sync /data gdrive:backup-daten \ --fast-list \ --drive-stop-on-upload-limit \ --tpslimit 25 \ --drive-pacer-min-sleep 10ms \ --retries 3 \ --exclude "**/*.db-journal" \ --exclude "**/*.db-wal" \ --exclude "**/*.db-shm" \ --exclude "gitea/gitea/gitea.db" \ --exclude "filebrowser/database.db" \ --exclude "daten/DJing/Data/MusicLibrary.db" \ --log-file=/var/log/rclone-backup.log \ --log-level=INFO ''; User = "root"; }; }; systemd.timers.rclone-backup = { wantedBy = [ "timers.target" ]; timerConfig = { OnCalendar = "03:00"; Persistent = true; }; }; ######################################## # System Packages ######################################## environment.systemPackages = [ pkgs.git pkgs.tmux pkgs.htop pkgs.podman-compose ]; ######################################## # Firewall ######################################## networking.firewall.enable = true; networking.firewall.allowedTCPPorts = [ 22 139 445 8080 5357 3000 2222 ]; # 5357 WSD networking.firewall.allowedUDPPorts = [ 137 138 3702 5353 ]; # NetBIOS + WSD (3702) + mDNS (5353) }