Add *arr media stack + Gitea to jupiter
- sabnzbd, prowlarr, sonarr, radarr, clonarr, seerr, cinephage, mediamanager services, wired into jupiter with LAN Caddy vhosts. - Gitea: migrated the old ZimaOS docker instance's data (sqlite db, 4 repos, no LFS objects) into the NixOS module's default stateDir layout. HTTP via Caddy; git SSH on its own built-in server at :2222 (not :222 - the unpriv gitea user can't bind <1024). - mediamanager-nix flake input for the mediamanager service. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Generated
+119
-3
@@ -20,6 +20,46 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"media-manager": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1767373218,
|
||||||
|
"narHash": "sha256-kMFxhRYhNTqmcB9uJV44FM9oAa4Wa+Vi4LQPnhEpUZ0=",
|
||||||
|
"owner": "maxdorninger",
|
||||||
|
"repo": "MediaManager",
|
||||||
|
"rev": "d420a2714c28f3d6a5651c437cfb8f2300463ed2",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "maxdorninger",
|
||||||
|
"repo": "MediaManager",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mediamanager-nix": {
|
||||||
|
"inputs": {
|
||||||
|
"media-manager": "media-manager",
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"pyproject-build-systems": "pyproject-build-systems",
|
||||||
|
"pyproject-nix": "pyproject-nix",
|
||||||
|
"uv2nix": "uv2nix"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1768002052,
|
||||||
|
"narHash": "sha256-s/BKe43bsICx6LiqRG2V5Ua2N7jQYLuI4+gLUY6zYjc=",
|
||||||
|
"owner": "strangeglyph",
|
||||||
|
"repo": "mediamanager-nix",
|
||||||
|
"rev": "867a9f1fd0f38eca33f72c37c30f82cd13d5c88c",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "strangeglyph",
|
||||||
|
"repo": "mediamanager-nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nixos-images": {
|
"nixos-images": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixos-stable": "nixos-stable",
|
"nixos-stable": "nixos-stable",
|
||||||
@@ -75,11 +115,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1783703440,
|
"lastModified": 1784280462,
|
||||||
"narHash": "sha256-O3/YajjWo001VUIgD8BwaRdSNLUFe7nZ1qV5TwhRBcw=",
|
"narHash": "sha256-DtoqIqM7VkR6NxAkcLpMwmi02USwWb3JdmNGLyhthc0=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "8f0500b9660505dc3cb647775fe9a978a74b5283",
|
"rev": "293d6abedf0478e681a4dfcfcb35b30fc796a32f",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -89,9 +129,60 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"pyproject-build-systems": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"mediamanager-nix",
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"pyproject-nix": [
|
||||||
|
"mediamanager-nix",
|
||||||
|
"pyproject-nix"
|
||||||
|
],
|
||||||
|
"uv2nix": [
|
||||||
|
"mediamanager-nix",
|
||||||
|
"uv2nix"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1763662255,
|
||||||
|
"narHash": "sha256-4bocaOyLa3AfiS8KrWjZQYu+IAta05u3gYZzZ6zXbT0=",
|
||||||
|
"owner": "pyproject-nix",
|
||||||
|
"repo": "build-system-pkgs",
|
||||||
|
"rev": "042904167604c681a090c07eb6967b4dd4dae88c",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "pyproject-nix",
|
||||||
|
"repo": "build-system-pkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pyproject-nix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"mediamanager-nix",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1764134915,
|
||||||
|
"narHash": "sha256-xaKvtPx6YAnA3HQVp5LwyYG1MaN4LLehpQI8xEdBvBY=",
|
||||||
|
"owner": "pyproject-nix",
|
||||||
|
"repo": "pyproject.nix",
|
||||||
|
"rev": "2c8df1383b32e5443c921f61224b198a2282a657",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "pyproject-nix",
|
||||||
|
"repo": "pyproject.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"disko": "disko",
|
"disko": "disko",
|
||||||
|
"mediamanager-nix": "mediamanager-nix",
|
||||||
"nixos-images": "nixos-images",
|
"nixos-images": "nixos-images",
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs",
|
||||||
"sops-nix": "sops-nix"
|
"sops-nix": "sops-nix"
|
||||||
@@ -116,6 +207,31 @@
|
|||||||
"repo": "sops-nix",
|
"repo": "sops-nix",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"uv2nix": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"mediamanager-nix",
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"pyproject-nix": [
|
||||||
|
"mediamanager-nix",
|
||||||
|
"pyproject-nix"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1767152253,
|
||||||
|
"narHash": "sha256-xxuRsew0pedwptVnhfru01xbe+dDhI+OY1kCFDMxBUs=",
|
||||||
|
"owner": "pyproject-nix",
|
||||||
|
"repo": "uv2nix",
|
||||||
|
"rev": "7a3eb140416318349ec58d2d4e81afe071bc9f03",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "pyproject-nix",
|
||||||
|
"repo": "uv2nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
|
|||||||
@@ -15,9 +15,13 @@
|
|||||||
url = "github:nix-community/nixos-images";
|
url = "github:nix-community/nixos-images";
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
|
mediamanager-nix = {
|
||||||
|
url = "github:strangeglyph/mediamanager-nix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, disko, sops-nix, nixos-images, ... }@inputs:
|
outputs = { self, nixpkgs, disko, sops-nix, nixos-images, mediamanager-nix, ... }@inputs:
|
||||||
let
|
let
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
in
|
in
|
||||||
|
|||||||
@@ -14,8 +14,19 @@
|
|||||||
../../services/caddy.nix
|
../../services/caddy.nix
|
||||||
../../services/tailscale.nix
|
../../services/tailscale.nix
|
||||||
../../services/jellyfin.nix
|
../../services/jellyfin.nix
|
||||||
|
../../services/sabnzbd.nix
|
||||||
|
../../services/prowlarr.nix
|
||||||
|
../../services/sonarr.nix
|
||||||
|
../../services/radarr.nix
|
||||||
|
../../services/clonarr.nix
|
||||||
|
../../services/seerr.nix
|
||||||
|
../../services/gitea.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 ----
|
# ---- Host identity ----
|
||||||
networking.hostName = "jupiter";
|
networking.hostName = "jupiter";
|
||||||
networking.networkmanager.enable = true;
|
networking.networkmanager.enable = true;
|
||||||
@@ -49,11 +60,17 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
# ---- Caddy vhosts (LAN) ----
|
# ---- Caddy vhosts (LAN) ----
|
||||||
# whoami on :80, audiobookshelf via a pihole local-DNS name -> jupiter IP.
|
# Reached via pihole local-DNS names -> jupiter IP.
|
||||||
services.caddy.virtualHosts = {
|
services.caddy.virtualHosts = {
|
||||||
"http://localhost".extraConfig = "reverse_proxy localhost:8080";
|
|
||||||
"http://audiobookshelf.jupiter.sol".extraConfig = "reverse_proxy localhost:8000";
|
"http://audiobookshelf.jupiter.sol".extraConfig = "reverse_proxy localhost:8000";
|
||||||
"http://jellyfin.jupiter.sol".extraConfig = "reverse_proxy localhost:8096";
|
"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";
|
||||||
};
|
};
|
||||||
|
|
||||||
system.stateVersion = "26.05";
|
system.stateVersion = "26.05";
|
||||||
|
|||||||
@@ -25,4 +25,5 @@
|
|||||||
|
|
||||||
# Headscale pre-auth key for tailscale auto-registration (see configuration.nix).
|
# Headscale pre-auth key for tailscale auto-registration (see configuration.nix).
|
||||||
sops.secrets.tailscale_authkey = { };
|
sops.secrets.tailscale_authkey = { };
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
samba_password: ENC[AES256_GCM,data:K3FtKC0CrOLMyfQokmxxlyUnDPo=,iv:9bTE/S/i05LJYldOHuBuz3+g8JdTuq0ysFFgXOmfti8=,tag:77NIxy7qpHRqCmbOR83AfA==,type:str]
|
samba_password: ENC[AES256_GCM,data:K3FtKC0CrOLMyfQokmxxlyUnDPo=,iv:9bTE/S/i05LJYldOHuBuz3+g8JdTuq0ysFFgXOmfti8=,tag:77NIxy7qpHRqCmbOR83AfA==,type:str]
|
||||||
darman_password: ENC[AES256_GCM,data:DOHHlM4Qdw4WgkN+/M51n2LMjJqq5MS0FeGEH8Pz3297yUZ8jBDOKRS/Tek9stC05JKEQRtn/vVVbAY0nXs20MwrsDMo+IEFXx7Ms90vVyYIYe5O5/0aQkPq84vcwGN0RW1Rj5Y3s38vWA==,iv:DAqHbvOBq7FT7ALbmBXJ0HadEGEsL8V2e7R99H5ZH0s=,tag:9XpiRFIbaZjMn59uHCOA1g==,type:str]
|
darman_password: ENC[AES256_GCM,data:DOHHlM4Qdw4WgkN+/M51n2LMjJqq5MS0FeGEH8Pz3297yUZ8jBDOKRS/Tek9stC05JKEQRtn/vVVbAY0nXs20MwrsDMo+IEFXx7Ms90vVyYIYe5O5/0aQkPq84vcwGN0RW1Rj5Y3s38vWA==,iv:DAqHbvOBq7FT7ALbmBXJ0HadEGEsL8V2e7R99H5ZH0s=,tag:9XpiRFIbaZjMn59uHCOA1g==,type:str]
|
||||||
tailscale_authkey: ENC[AES256_GCM,data:GJjN9zdU3zNBxA8cEBdsmjDrj8G/m04r8JRCEDZkdd7jYplEHyLfhbMcIyZIBe3Sh4d3PM/PcKb191LZoMg/b1G4hiVwciaCoMY0O9kAhRg/aYHXluSIBQ==,iv:R3OsZ+3to0tBhF4IaaSHm0afkw5o0KfXkcTdT9HFucg=,tag:QQNXC9ya88lO087iGIc1sw==,type:str]
|
tailscale_authkey: ENC[AES256_GCM,data:GJjN9zdU3zNBxA8cEBdsmjDrj8G/m04r8JRCEDZkdd7jYplEHyLfhbMcIyZIBe3Sh4d3PM/PcKb191LZoMg/b1G4hiVwciaCoMY0O9kAhRg/aYHXluSIBQ==,iv:R3OsZ+3to0tBhF4IaaSHm0afkw5o0KfXkcTdT9HFucg=,tag:QQNXC9ya88lO087iGIc1sw==,type:str]
|
||||||
|
mediamanager_token_secret: ENC[AES256_GCM,data:g75vj1E6B029O076yV3DS/1z99Tq6wMhEVx+ULYDjHsplyA+vqVRvBlviC64V47IMqKd9k2eTGBJ98Ptv3UIjbr446xZinz0c7PZgHU0XOX4EUcB1ORloMFZIv1zUv8VRjUISUQnn/vRk60e7u8eXrOrgXjxfnoBKrdKZmqxhAM=,iv:2t0XBExC9RvbTomezka+99/LtJl64zNwneA7WWc4ju0=,tag:SVCYuw0n6JevnvoQOIkjPw==,type:str]
|
||||||
|
sabnzbd_api_key: ENC[AES256_GCM,data:6UW1u2Ikmnq34t4H4k/4C44SJeFHRlaPjWwUjEfH1GQ=,iv:sGsd8Sd2pfUhTUDg6PlRzfVYejRbF69jmDTIa2fvY4M=,tag:3fOLgU1K1gKHxQ3J1+3oRQ==,type:str]
|
||||||
|
prowlarr_api_key: ENC[AES256_GCM,data:ab1QACaagI8ACJXFUy8r9X9lgYwRo1byUmhBPSRWwKk=,iv:EcuF6EN/4mWxlXi6R1qDzv4rOw6AT+OGSNQaaBwjJHg=,tag:hixHrbQWU6QQZNMM5rNDsg==,type:str]
|
||||||
|
cinephage_better_auth_secret: ENC[AES256_GCM,data:S1ilcQeC2HmXe/4xdLi6wm5RNz954SL3qVur6JCn5ekBVCbXMd1DGCafjhU=,iv:9rS5gDuazMOAq/hWp0onvHZPzKJgQM3oWIrtplJN/9I=,tag:xNqf/unY2v/98p4v52vUqw==,type:str]
|
||||||
sops:
|
sops:
|
||||||
age:
|
age:
|
||||||
- enc: |
|
- enc: |
|
||||||
@@ -21,7 +25,7 @@ sops:
|
|||||||
CzjSDQZTcseEXZNwuzZcfB5Mvq0BQvjOj7lGuxzuE4qwWkdJWGfVLQ==
|
CzjSDQZTcseEXZNwuzZcfB5Mvq0BQvjOj7lGuxzuE4qwWkdJWGfVLQ==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
recipient: age1zak7glavmg4026p2389fyqe769vqm4jrryknuqckgqq4merz5f7q44rkkt
|
recipient: age1zak7glavmg4026p2389fyqe769vqm4jrryknuqckgqq4merz5f7q44rkkt
|
||||||
lastmodified: "2026-07-12T22:19:04Z"
|
lastmodified: "2026-07-18T00:02:37Z"
|
||||||
mac: ENC[AES256_GCM,data:iDcCp2dE657x9xDGM4Ki76gycaOM/7Z8v7qdl3Bcf0HcbH0oKWE2nShBO8/YK67p+Dkhghp/83G8vXCmEo0MA83C+gHha6JAyX2UJtvtVhBgF0ul8LMnXt4l+l7Ps8Bjne+LLtUK3U/2LGvADANPjNbpbCwe6x9OUPHmPXWYDxo=,iv:Cnrxb3xPCvq52DoktC1Osv1WbF/I7wfIYbAVRi2eNg4=,tag:Yodqbtl1+DnDvrtgT1lawQ==,type:str]
|
mac: ENC[AES256_GCM,data:By7/CkQ8bftrGzXwaD9gl1G/iV5AuJEnxHUAK1wMivzUj4mjTvtd3m/RaDd9aFuplNREKBAvUuH4WJ4RrrK+NTNqEsy+VUxch4n/3QC0TSjiEersJR8GpSbfa+AlZ7xNxS11AvV0NGW0PZzxIToR75Yn+hW4SoqeoLy+Nr3b+3U=,iv:sBdbOZl7JbzcR7kq0cyMxv9RATIzs0SheoC32H38iWA=,tag:XeL54IV5xK8+Oa7JhWPTkw==,type:str]
|
||||||
unencrypted_suffix: _unencrypted
|
unencrypted_suffix: _unencrypted
|
||||||
version: 3.13.1
|
version: 3.13.1
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{ config, ... }:
|
||||||
|
|
||||||
|
# Cinephage — indexer search + streaming/library manager. Runs the official
|
||||||
|
# container image, not upstream's nix flake module: their flake's
|
||||||
|
# npmDepsHash is stale against their own package-lock.json (fixed-output
|
||||||
|
# hash mismatch), and past that, a transitive dep's postinstall hard-enforces
|
||||||
|
# pnpm (`only-allow pnpm`), which fails under nix's network-sandboxed npm
|
||||||
|
# build regardless. Docker is their actually-maintained deployment path.
|
||||||
|
# Paired sops secret in hosts/jupiter/secrets.nix — BETTER_AUTH_SECRET signs
|
||||||
|
# sessions/encrypts stored API keys; losing it invalidates all sessions and
|
||||||
|
# makes saved keys unrecoverable, so it must be static, not app-generated.
|
||||||
|
{
|
||||||
|
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 -"
|
||||||
|
];
|
||||||
|
}
|
||||||
+3
-11
@@ -1,7 +1,8 @@
|
|||||||
{ ... }:
|
{ ... }:
|
||||||
|
|
||||||
# Podman (Docker-compatible) + declarative OCI containers.
|
# Podman (Docker-compatible) + declarative OCI containers.
|
||||||
# Copy the whoami block per app; publish ports and mount /mnt/data volumes.
|
# Add containers under virtualisation.oci-containers.containers; publish
|
||||||
|
# ports and mount /mnt/data volumes as needed.
|
||||||
{
|
{
|
||||||
virtualisation.podman = {
|
virtualisation.podman = {
|
||||||
enable = true;
|
enable = true;
|
||||||
@@ -9,14 +10,5 @@
|
|||||||
defaultNetwork.settings.dns_enabled = true;
|
defaultNetwork.settings.dns_enabled = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
virtualisation.oci-containers = {
|
virtualisation.oci-containers.backend = "podman";
|
||||||
backend = "podman";
|
|
||||||
containers = {
|
|
||||||
whoami = {
|
|
||||||
image = "traefik/whoami:latest";
|
|
||||||
ports = [ "8080:80" ];
|
|
||||||
autoStart = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
{ ... }:
|
||||||
|
|
||||||
|
# Gitea — self-hosted git. stateDir/repositories were migrated from the old
|
||||||
|
# ZimaOS docker instance (single user/org "darman", sqlite db, 4 repos, no
|
||||||
|
# LFS objects yet) into stateDir's default layout (data/gitea.db,
|
||||||
|
# data/{indexers,avatars,sessions,...}, repositories/), so no import step is
|
||||||
|
# needed on first boot — NixOS just needs to chown it to the gitea user
|
||||||
|
# (see below).
|
||||||
|
#
|
||||||
|
# HTTP is reverse-proxied through Caddy (see hosts/jupiter/configuration.nix
|
||||||
|
# for the vhost). SSH clone/push uses gitea's own built-in SSH server on
|
||||||
|
# :2222, kept separate from the host's OpenSSH on :22. NOT :222 (the old
|
||||||
|
# docker setup's external port) — gitea runs as the unpriv "gitea" user with
|
||||||
|
# an empty capability set, so it can't bind <1024: "listen tcp :222: bind:
|
||||||
|
# permission denied".
|
||||||
|
#
|
||||||
|
# After first deploy, fix ownership of the migrated data (it currently
|
||||||
|
# belongs to darman:users from the CIFS copy):
|
||||||
|
# chown -R gitea:gitea /mnt/data/AppData/gitea
|
||||||
|
{
|
||||||
|
services.gitea = {
|
||||||
|
enable = true;
|
||||||
|
stateDir = "/mnt/data/AppData/gitea";
|
||||||
|
lfs.enable = true;
|
||||||
|
|
||||||
|
settings = {
|
||||||
|
repository = {
|
||||||
|
DEFAULT_BRANCH = "master";
|
||||||
|
};
|
||||||
|
server = {
|
||||||
|
DOMAIN = "git.mgaction.town";
|
||||||
|
SSH_DOMAIN = "git.mgaction.town";
|
||||||
|
ROOT_URL = "http://git.mgaction.town/";
|
||||||
|
HTTP_PORT = 3000;
|
||||||
|
START_SSH_SERVER = true;
|
||||||
|
SSH_PORT = 2222;
|
||||||
|
SSH_LISTEN_PORT = 2222;
|
||||||
|
};
|
||||||
|
service = {
|
||||||
|
DISABLE_REGISTRATION = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
networking.firewall.allowedTCPPorts = [ 2222 ];
|
||||||
|
|
||||||
|
users.users.gitea.extraGroups = [ "users" ];
|
||||||
|
}
|
||||||
+10
-8
@@ -1,16 +1,18 @@
|
|||||||
{ ... }:
|
{ lib, ... }:
|
||||||
|
|
||||||
# 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.jellyfin = {
|
services.jellyfin = {
|
||||||
enable = true;
|
enable = true;
|
||||||
dataDir = "/mnt/data/AppData/jellyfin";
|
dataDir = "/mnt/data/AppData/jellyfin";
|
||||||
cacheDir = "${cfg.dataDir}/cache";
|
cacheDir = "/mnt/data/AppData/jellyfin/cache";
|
||||||
};
|
};
|
||||||
users.users.jellyfin.extraGroups = [ "users" ];
|
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,54 @@
|
|||||||
|
{ config, ... }:
|
||||||
|
|
||||||
|
# MediaManager — media request/library manager. Module comes from the
|
||||||
|
# community flake input `mediamanager-nix` (wired into jupiter's module list
|
||||||
|
# in flake.nix, NOT nixpkgs). Paired sops secret declared in
|
||||||
|
# hosts/jupiter/secrets.nix — without it the module mints+discards a random
|
||||||
|
# auth token_secret on every service 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,30 @@
|
|||||||
|
{ ... }:
|
||||||
|
|
||||||
|
# Prowlarr — indexer manager (usenet + torrent), feeds SABnzbd/MediaManager.
|
||||||
|
# services.prowlarr.dataDir is left at its module default (/var/lib/prowlarr)
|
||||||
|
# on purpose: passing a *custom* dataDir makes the upstream module bind-mount
|
||||||
|
# it and force-reset the outer dir to 0700 root:root via a tmpfiles rule on
|
||||||
|
# every boot (nixos/modules/services/misc/servarr/prowlarr.nix) — that stomps
|
||||||
|
# DynamicUser's access to pre-existing content and causes intermittent
|
||||||
|
# "unable to open database file".
|
||||||
|
#
|
||||||
|
# Instead we bind-mount the real (migrated-from-ZimaOS) config dir straight
|
||||||
|
# onto the module's own default path, so prowlarr never sees a "custom"
|
||||||
|
# dataDir and none of that logic triggers. DynamicUser+StateDirectory then
|
||||||
|
# recursively chowns the pre-existing content to its assigned uid on first
|
||||||
|
# activation, same as it does for a fresh install — no manual chown needed.
|
||||||
|
#
|
||||||
|
# Mount onto /var/lib/private/prowlarr, NOT the public /var/lib/prowlarr:
|
||||||
|
# DynamicUser+StateDirectory keeps real data at .../private/<name> and makes
|
||||||
|
# the public path a symlink to it; binding onto the public path turns it into
|
||||||
|
# a mountpoint systemd then can't rename during its migrate-on-start dance
|
||||||
|
# ("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,46 @@
|
|||||||
|
{ ... }:
|
||||||
|
|
||||||
|
# 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 single job —
|
||||||
|
# independent of the ini's `umask` setting (that only covers files
|
||||||
|
# created during unpack, not the enclosing per-job dir). setgid on
|
||||||
|
# Downloads keeps the *group* as "users", but group perm bits still come
|
||||||
|
# back zeroed, locking out cinephage/mediamanager. Sweep it clean instead
|
||||||
|
# of fighting SABnzbd's own behavior.
|
||||||
|
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,30 @@
|
|||||||
|
{ ... }:
|
||||||
|
|
||||||
|
# Seerr (formerly Jellyseerr) — request manager for Jellyfin, talks to
|
||||||
|
# Sonarr/Radarr to fulfill requests. Fresh install, no migrated data.
|
||||||
|
#
|
||||||
|
# configDir is left at the module default (/var/lib/seerr) on purpose:
|
||||||
|
# systemd's StateDirectory= auto-chown for DynamicUser only applies to that
|
||||||
|
# exact default path, not to an arbitrary custom one (same class of issue as
|
||||||
|
# prowlarr.nix, worked around the same way) — bind-mount AppData onto the
|
||||||
|
# default path instead of overriding configDir, so it stays on the RAID array
|
||||||
|
# and survives an OS-disk reinstall like every other service's data.
|
||||||
|
#
|
||||||
|
# Mount onto /var/lib/private/seerr, NOT the public /var/lib/seerr:
|
||||||
|
# DynamicUser+StateDirectory keeps real data at .../private/<name> and makes
|
||||||
|
# the public path a symlink to it; binding onto the public path turns it into
|
||||||
|
# a mountpoint systemd then can't rename during its migrate-on-start dance
|
||||||
|
# ("Device or resource busy", exit 238/STATE_DIRECTORY).
|
||||||
|
{
|
||||||
|
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