Reorganize services/ into category subfolders
Group service modules by category (media, network, vpn, identity, dev, desktop) to make the growing services/ dir easier to navigate. containers.nix stays at the top level since it's a shared backend, not a single-category service. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
{ ... }:
|
||||
|
||||
# Audiobookshelf audiobook/podcast server.
|
||||
# Listens on all interfaces: :8000 stays closed on the LAN (no openFirewall),
|
||||
# but reachable over the trusted tailscale0 interface and via localhost (caddy).
|
||||
# Library/media paths are set in the web UI — point them at /mnt/data/...
|
||||
# Runs as user `audiobookshelf`; added to `users` so it can read group-owned
|
||||
# library dirs on the RAID.
|
||||
{
|
||||
services.audiobookshelf = {
|
||||
enable = true;
|
||||
host = "0.0.0.0";
|
||||
port = 8000;
|
||||
};
|
||||
users.users.audiobookshelf.extraGroups = [ "users" ];
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{ config, ... }:
|
||||
|
||||
# Cinephage — indexer search + streaming/library manager. Runs the official
|
||||
# container image, not upstream's nix flake module: its npmDepsHash is stale
|
||||
# against its own package-lock.json, and a transitive dep hard-enforces pnpm,
|
||||
# breaking the nix-sandboxed npm build regardless. Docker is the actually-
|
||||
# maintained path. BETTER_AUTH_SECRET (paired sops secret in
|
||||
# hosts/jupiter/secrets.nix) signs sessions/encrypts stored API keys — must
|
||||
# be static, not app-generated, or losing it invalidates everything.
|
||||
{
|
||||
virtualisation.oci-containers.containers.cinephage = {
|
||||
image = "ghcr.io/moldytaint/cinephage:latest";
|
||||
autoStart = true;
|
||||
# Host networking, not a published port: cinephage needs to reach
|
||||
# Prowlarr/SABnzbd on jupiter's own localhost (they're native systemd
|
||||
# services, not containers) — bridge-mode "localhost" would be the
|
||||
# container's own netns, not the host's.
|
||||
extraOptions = [ "--network=host" ];
|
||||
volumes = [
|
||||
"/mnt/data/AppData/cinephage:/config"
|
||||
"/mnt/data/HighSeas:/media"
|
||||
"/mnt/data/HighSeas/Downloads:/downloads"
|
||||
];
|
||||
environment = {
|
||||
PUID = "1000";
|
||||
PGID = "100"; # darman:users — matches HighSeas' real on-disk ownership
|
||||
TZ = "Europe/Berlin";
|
||||
ORIGIN = "http://cinephage.jupiter.sol";
|
||||
};
|
||||
environmentFiles = [ config.sops.templates."cinephage.env".path ];
|
||||
};
|
||||
|
||||
sops.templates."cinephage.env".content =
|
||||
"BETTER_AUTH_SECRET=${config.sops.placeholder.cinephage_better_auth_secret}";
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /mnt/data/AppData/cinephage 0755 darman users -"
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{ ... }:
|
||||
|
||||
# Clonarr — visual TRaSH-Guides sync tool for Radarr/Sonarr (quality
|
||||
# profiles, custom formats, scores). No nixpkgs package; runs the official
|
||||
# container image (ghcr.io/prophetse7en/clonarr).
|
||||
{
|
||||
virtualisation.oci-containers.containers.clonarr = {
|
||||
image = "ghcr.io/prophetse7en/clonarr:latest";
|
||||
autoStart = true;
|
||||
# Host networking: clonarr needs to reach Radarr/Sonarr/Prowlarr on
|
||||
# jupiter's own localhost (native systemd services, not containers) —
|
||||
# bridge-mode "localhost" would be the container's own netns, not the host's.
|
||||
extraOptions = [ "--network=host" ];
|
||||
volumes = [
|
||||
"/mnt/data/AppData/clonarr:/config"
|
||||
];
|
||||
environment = {
|
||||
TZ = "Europe/Berlin";
|
||||
PUID = "1000";
|
||||
PGID = "100"; # darman:users
|
||||
PORT = "6060";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /mnt/data/AppData/clonarr 0755 darman users -"
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{ lib, ... }:
|
||||
|
||||
{
|
||||
services.jellyfin = {
|
||||
enable = true;
|
||||
dataDir = "/mnt/data/AppData/jellyfin";
|
||||
cacheDir = "/mnt/data/AppData/jellyfin/cache";
|
||||
};
|
||||
users.users.jellyfin.extraGroups = [ "users" ];
|
||||
|
||||
# The upstream module hardcodes UMask=0077 — root cause of jellyfin writing
|
||||
# trickplay thumbnails into stray new show folders it invented itself,
|
||||
# owned jellyfin:jellyfin 700, invisible to every other service sharing
|
||||
# the library (cinephage, mediamanager, ...). New files/dirs it creates
|
||||
# from here on inherit group "users" (library roots are setgid, see the
|
||||
# one-time chmod g+s done by hand) and stay group-writable.
|
||||
systemd.services.jellyfin.serviceConfig.UMask = lib.mkForce "0002";
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{ config, ... }:
|
||||
|
||||
# MediaManager — media request/library manager. Module comes from the
|
||||
# community flake input `mediamanager-nix`, not nixpkgs. Paired sops secret
|
||||
# in hosts/jupiter/secrets.nix — without it the module mints+discards a
|
||||
# random auth token_secret on every restart, logging everyone out.
|
||||
# Port 8010: 8000 is taken by audiobookshelf on this host.
|
||||
{
|
||||
services.media-manager = {
|
||||
enable = true;
|
||||
dataDir = "/mnt/data/AppData/mediamanager";
|
||||
host = "0.0.0.0";
|
||||
port = 8010;
|
||||
postgres.enable = true;
|
||||
environmentFile = config.sops.templates."mediamanager.env".path;
|
||||
settings = {
|
||||
misc = {
|
||||
frontend_url = "http://mediamanager.jupiter.sol";
|
||||
# Point straight at the existing library instead of the empty
|
||||
# dirs under dataDir — group "users" needs write access (see
|
||||
# the chmod note below); files stay darman-owned.
|
||||
movie_directory = "/mnt/data/HighSeas/Movies";
|
||||
tv_directory = "/mnt/data/HighSeas/Shows";
|
||||
image_directory = "/mnt/data/HighSeas/images";
|
||||
torrent_directory = "/mnt/data/HighSeas/Downloads";
|
||||
};
|
||||
auth.admin_emails = [ "mail@erik-s.dev" ];
|
||||
# API keys are secret -> env vars via the sops template below, not here
|
||||
# (settings.* is rendered to a world-readable file in /nix/store).
|
||||
torrents.sabnzbd = {
|
||||
enabled = true;
|
||||
host = "http://localhost";
|
||||
port = 8085;
|
||||
};
|
||||
indexers.prowlarr = {
|
||||
enabled = true;
|
||||
url = "http://localhost:9696";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
sops.templates."mediamanager.env".content = ''
|
||||
MEDIAMANAGER_AUTH__TOKEN_SECRET=${config.sops.placeholder.mediamanager_token_secret}
|
||||
MEDIAMANAGER_TORRENTS__SABNZBD__API_KEY=${config.sops.placeholder.sabnzbd_api_key}
|
||||
MEDIAMANAGER_INDEXERS__PROWLARR__API_KEY=${config.sops.placeholder.prowlarr_api_key}
|
||||
'';
|
||||
|
||||
# HighSeas/{Movies,Shows,images,Downloads} are darman:users 755 on disk —
|
||||
# group has no write bit. media-manager is in "users" (below); the dirs
|
||||
# themselves were chmod g+w by hand once (not declarative — see CLAUDE.md
|
||||
# gotchas), since this is pre-existing data, not something tmpfiles owns.
|
||||
users.users.media-manager.extraGroups = [ "users" ];
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{ ... }:
|
||||
|
||||
# Prowlarr — indexer manager (usenet + torrent), feeds SABnzbd/MediaManager.
|
||||
# dataDir is left at the module default: a *custom* dataDir makes the
|
||||
# upstream module force-reset it to 0700 root:root on every boot, stomping
|
||||
# DynamicUser's access ("unable to open database file"). Instead bind-mount
|
||||
# the real (migrated-from-ZimaOS) config dir onto the default path, so
|
||||
# DynamicUser+StateDirectory chowns it on first activation like a fresh
|
||||
# install — no manual chown needed.
|
||||
#
|
||||
# Mount onto /var/lib/private/prowlarr, NOT the public /var/lib/prowlarr:
|
||||
# StateDirectory symlinks the public path to .../private/<name>; binding
|
||||
# onto the public path itself blocks systemd's migrate-on-start rename
|
||||
# ("Device or resource busy", exit 238/STATE_DIRECTORY).
|
||||
{
|
||||
services.prowlarr.enable = true;
|
||||
|
||||
fileSystems."/var/lib/private/prowlarr" = {
|
||||
device = "/mnt/data/AppData/prowlarr/config";
|
||||
fsType = "none";
|
||||
options = [ "bind" ];
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{ ... }:
|
||||
|
||||
# Radarr — movie library manager, feeds off SABnzbd/Prowlarr. dataDir points
|
||||
# at the config migrated from the old ZimaOS docker stack (indexers/download
|
||||
# client/history already set up). Unlike prowlarr, this module uses a static
|
||||
# `radarr` user (no DynamicUser) and only auto-chowns dataDir when it's the
|
||||
# module's own default path — since we point at a pre-existing migrated dir,
|
||||
# chown it by hand once after first deploy:
|
||||
# chown -R radarr:radarr /mnt/data/AppData/radarr/config
|
||||
{
|
||||
services.radarr = {
|
||||
enable = true;
|
||||
dataDir = "/mnt/data/AppData/radarr/config";
|
||||
};
|
||||
|
||||
# Write access to the shared library/downloads dirs (owned darman:users).
|
||||
users.users.radarr.extraGroups = [ "users" ];
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{ ... }:
|
||||
|
||||
# SABnzbd — usenet downloader. Reuses the config migrated from the old
|
||||
# ZimaOS docker stack (servers/API key/history already set up) by pointing
|
||||
# straight at the real ini instead of generating a fresh NixOS-managed one.
|
||||
# Runs as the module's default dedicated `sabnzbd` system user — after first
|
||||
# deploy, chown the migrated config dir to it (see README/CLAUDE notes):
|
||||
# chown -R sabnzbd:sabnzbd /mnt/data/AppData/sabnzbd/config
|
||||
{
|
||||
services.sabnzbd = {
|
||||
enable = true;
|
||||
configFile = "/mnt/data/AppData/sabnzbd/config/sabnzbd.ini";
|
||||
allowConfigWrite = true; # real pre-existing state — let sabnzbd keep saving it
|
||||
};
|
||||
|
||||
# Write access to the shared downloads dir (owned darman:users on disk).
|
||||
users.users.sabnzbd.extraGroups = [ "users" ];
|
||||
|
||||
# SABnzbd hardcodes completed job folders to 0700 on every job, ignoring
|
||||
# the ini's `umask` (that only covers files during unpack, not the job
|
||||
# dir itself). setgid on Downloads keeps the group as "users" but perm
|
||||
# bits still come back zeroed, locking out cinephage/mediamanager — sweep
|
||||
# it clean instead of fighting SABnzbd.
|
||||
systemd.services.fix-downloads-perms = {
|
||||
description = "Fix group perms SABnzbd resets on completed downloads";
|
||||
serviceConfig.Type = "oneshot";
|
||||
script = ''
|
||||
find /mnt/data/HighSeas/Downloads \
|
||||
! -group users -exec chgrp users {} + 2>/dev/null || true
|
||||
find /mnt/data/HighSeas/Downloads -type d ! -perm -g+rwx \
|
||||
-exec chmod g+rwx {} + 2>/dev/null || true
|
||||
find /mnt/data/HighSeas/Downloads -type f ! -perm -g+rw \
|
||||
-exec chmod g+rw {} + 2>/dev/null || true
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.timers.fix-downloads-perms = {
|
||||
description = "Periodically fix group perms under HighSeas/Downloads";
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnBootSec = "1m";
|
||||
OnUnitActiveSec = "2m";
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{ ... }:
|
||||
|
||||
# Seerr (formerly Jellyseerr) — request manager for Jellyfin, talks to
|
||||
# Sonarr/Radarr to fulfill requests. Fresh install, no migrated data.
|
||||
#
|
||||
# configDir stays at the module default; bind-mount AppData onto it instead
|
||||
# of overriding configDir, so data lives on the RAID array and survives an
|
||||
# OS-disk reinstall (same DynamicUser/StateDirectory issue as prowlarr.nix —
|
||||
# see that file for why, and why the mount targets /var/lib/private/seerr
|
||||
# rather than the public path).
|
||||
{
|
||||
services.seerr.enable = true;
|
||||
|
||||
fileSystems."/var/lib/private/seerr" = {
|
||||
device = "/mnt/data/AppData/seerr";
|
||||
fsType = "none";
|
||||
options = [ "bind" ];
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /mnt/data/AppData/seerr 0755 darman users -"
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{ ... }:
|
||||
|
||||
# Sonarr — TV library manager, feeds off SABnzbd/Prowlarr. dataDir points at
|
||||
# the config migrated from the old ZimaOS docker stack (indexers/download
|
||||
# client/history already set up). Unlike prowlarr, this module uses a static
|
||||
# `sonarr` user (no DynamicUser) and only auto-chowns dataDir when it's the
|
||||
# module's own default path — since we point at a pre-existing migrated dir,
|
||||
# chown it by hand once after first deploy:
|
||||
# chown -R sonarr:sonarr /mnt/data/AppData/sonarr/config
|
||||
{
|
||||
services.sonarr = {
|
||||
enable = true;
|
||||
dataDir = "/mnt/data/AppData/sonarr/config";
|
||||
};
|
||||
|
||||
# Write access to the shared library/downloads dirs (owned darman:users).
|
||||
users.users.sonarr.extraGroups = [ "users" ];
|
||||
}
|
||||
Reference in New Issue
Block a user