35c02d6920
Copying gitea.db / filebrowser database.db / MusicLibrary.db while they are being written fails md5 verification every run (the file changes mid -transfer). Take a consistent sqlite .backup into /data/db-snapshots and exclude the live DB files from the sync, so the job completes cleanly and the backup holds restorable DB copies. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
368 lines
13 KiB
Nix
368 lines
13 KiB
Nix
{ 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 = ''
|
|
<?xml version="1.0" standalone='no'?>
|
|
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
|
|
<service-group>
|
|
<name replace-wildcards="yes">%h</name>
|
|
<service>
|
|
<type>_smb._tcp</type>
|
|
<port>445</port>
|
|
</service>
|
|
<service>
|
|
<type>_device-info._tcp</type>
|
|
<port>0</port>
|
|
<txt-record>model=TimeCapsule8,119</txt-record>
|
|
</service>
|
|
<service>
|
|
<type>_adisk._tcp</type>
|
|
<port>9</port>
|
|
<txt-record>sys=waMa=0,adVF=0x100</txt-record>
|
|
<txt-record>dk0=adVN=timemachine,adVF=0x82</txt-record>
|
|
</service>
|
|
</service-group>
|
|
'';
|
|
};
|
|
};
|
|
|
|
# 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')"
|
|
${pkgs.sqlite}/bin/sqlite3 "$db" ".timeout 15000" ".backup '$out'" \
|
|
|| echo "WARN: snapshot of $db failed" >&2
|
|
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)
|
|
} |