diff --git a/modules/nixos/server/options.nix b/modules/nixos/server/options.nix index 4de3252..06e8808 100644 --- a/modules/nixos/server/options.nix +++ b/modules/nixos/server/options.nix @@ -14,7 +14,7 @@ in { description = "The server profile the host will use as a base"; }; services = mkOption { - type = listOf (enum ["media-server" "website" "forgejo"]); + type = listOf (enum ["media-server" "website" "forgejo" "ookflix"]); default = []; description = "List of services the server will host"; }; diff --git a/modules/nixos/server/services/default.nix b/modules/nixos/server/services/default.nix index 7685461..468b980 100644 --- a/modules/nixos/server/services/default.nix +++ b/modules/nixos/server/services/default.nix @@ -3,5 +3,6 @@ ./website ./forgejo ./media-server + ./ookflix ]; } diff --git a/modules/nixos/server/services/ookflix/default.nix b/modules/nixos/server/services/ookflix/default.nix new file mode 100644 index 0000000..8b99d22 --- /dev/null +++ b/modules/nixos/server/services/ookflix/default.nix @@ -0,0 +1,24 @@ +{ + lib, + config, + ... +}: let + inherit (lib) elem mkIf; + inherit (config.ooknet.server) services; +in { + imports = [ + ./jellyfin.nix + ./plex.nix + ./options.nix + ]; + + config = mkIf (elem "ookflix" services) { + ooknet.server.ookflix = { + gpuAcceleration.enable = true; + services = { + jellyfin.enable = true; + plex.enable = true; + }; + }; + }; +} diff --git a/modules/nixos/server/services/ookflix/jellyfin.nix b/modules/nixos/server/services/ookflix/jellyfin.nix new file mode 100644 index 0000000..cf6738f --- /dev/null +++ b/modules/nixos/server/services/ookflix/jellyfin.nix @@ -0,0 +1,60 @@ +{ + config, + lib, + ook, + ... +}: let + ookflixLib = import ./lib.nix {inherit lib config;}; + inherit (ookflixLib) mkServiceStateDir mkServiceUser; + inherit (lib) mkIf optionalAttrs; + inherit (ook.lib.container) mkContainerLabel mkContainerEnvironment mkContainerPort; + inherit (config.ooknet.server.ookflix) volumes services groups gpuAcceleration; + inherit (config.ooknet.server.ookflix.services) jellyfin; +in { + config = mkIf services.jellyfin.enable { + hardware.nvidia-container-toolkit.enable = gpuAcceleration.enable && gpuAcceleration.type == "nvidia"; + users = mkServiceUser jellyfin.user.name; + systemd.tmpfiles = mkServiceStateDir "jellyfin" jellyfin.stateDir; + virtualisation.oci-containers.containers = { + # media streaming server + # docs: + jellyfin = { + image = "lscr.io/linuxserver/jellyfin:latest"; + autoStart = true; + hostname = "jellyfin"; + ports = [(mkContainerPort jellyfin.port)]; + volumes = [ + "${volumes.media.movies}:/data/movies" + "${volumes.media.tv}:/data/tv" + "${jellyfin.stateDir}:/config" + ]; + labels = mkContainerLabel { + name = "jellyfin"; + inherit (jellyfin) port domain; + homepage = { + group = "media"; + description = "media-server streamer"; + }; + }; + + extraOptions = optionalAttrs gpuAcceleration.enable ( + if gpuAcceleration.type == "nvidia" + then [ + "--runtime=nvidia" + ] + else if gpuAcceleration.type == "intel" || "amd" + then [ + "--device=/dev/dri:/dev/dri" + ] + else [] + ); + environment = + mkContainerEnvironment jellyfin.user.id groups.media.id + // {JELLYFIN_PublishedServerUrl = jellyfin.domain;} + // optionalAttrs (gpuAcceleration.enable && gpuAcceleration.type == "nvidia") { + NVIDIA_VISIBLE_DEVICES = "all"; + }; + }; + }; + }; +} diff --git a/modules/nixos/server/services/ookflix/jellyseer.nix b/modules/nixos/server/services/ookflix/jellyseer.nix new file mode 100644 index 0000000..382c63d --- /dev/null +++ b/modules/nixos/server/services/ookflix/jellyseer.nix @@ -0,0 +1,32 @@ +{ + config, + lib, + ook, + ... +}: let + inherit (lib) mkIf; + inherit (ook.lib.container) mkContainerLabel mkContainerEnvironment mkContainerPort; + inherit (config.ooknet.server.ookflix) storage groups; + inherit (config.ooknet.server.ookflix.services) jellyseer; +in { + config = mkIf jellyseer.enable { + # media requesting for jellyfin + jellyseer = { + image = "fallenbagel/jellyseerr:latest"; + autoStart = true; + hostname = "jellyseer"; + ports = [(mkContainerPort jellyseer.port)]; + volumes = ["${storage.state.jellyseer}:/config"]; + extraOptions = ["--network" "host"]; + labels = mkContainerLabel { + name = "jellyseer"; + inherit (jellyseer) domain port; + homepage = { + group = "media"; + description = "media-server requesting"; + }; + }; + environment = mkContainerEnvironment jellyseer.user.id groups.media.id; + }; + }; +} diff --git a/modules/nixos/server/services/ookflix/lib.nix b/modules/nixos/server/services/ookflix/lib.nix new file mode 100644 index 0000000..ba25024 --- /dev/null +++ b/modules/nixos/server/services/ookflix/lib.nix @@ -0,0 +1,111 @@ +{ + lib, + config, + ... +}: let + inherit (lib) mkOption mkEnableOption elem assertMsg; + inherit (builtins) attrValues; + inherit (lib.types) int path port str; + inherit (config.ooknet) server; + cfg = server.ookflix; + + mkSubdomainOption = name: description: example: + mkOption { + type = str; + default = "${name}.${server.domain}"; + inherit description example; + }; + + # check config.ids for static uid/gid based on service name, if available use that, if not, use fallback. + # check fallback doesnt conflict with existing static id + getId = idType: name: fallback: let + allNixosIds = attrValues config.ids.${idType}; + fallbackConflict = elem fallback allNixosIds; + in + mkOption { + type = int; + default = + config.ids.${idType}.${name} + or ( + assert assertMsg (!fallbackConflict) + "Fallback ${idType} ${toString fallback} for ${name} conflicts with NixOS static allocation"; fallback + ); + description = "${idType} of ${name} container"; + example = 224; + }; + + mkUserOption = name: fallback: { + name = mkOption { + type = str; + default = name; + description = "Name of ${name} container user"; + example = "${name}"; + }; + id = getId "uids" name fallback; + }; + + mkGroupOption = name: fallback: { + name = mkOption { + type = str; + default = name; + }; + id = getId "gids" name fallback; + }; + + mkPortOption = value: description: example: + mkOption { + type = port; + default = value; + inherit description example; + }; + + mkVolumeOption = type: pathValue: + mkOption { + type = path; + default = + if type == "state" + then "${cfg.volumes.state.root}/${pathValue}" + else if type == "media" + then "${cfg.volumes.media.root}/${pathValue}" + else if type == "downloads" + then "${cfg.volumes.downloads.root}/${pathValue}" + else if type == "root" + then pathValue + else throw "Invalid VolumeOption type: ${type}. Must be one of 'state' 'media' 'downloads' 'root'"; + }; + + mkServiceOptions = name: { + port, + gid, + uid, + ... + } @ args: { + enable = mkEnableOption "Enable ${name} container"; + port = mkPortOption args.port "Port for ${name} container." 80; + domain = mkSubdomainOption name "Domain for ${name} container." "${name}.mydomain.com"; + stateDir = mkVolumeOption "state" name; + user = mkUserOption name args.uid; + group = mkGroupOption name args.gid; + }; + mkServiceUser = service: { + users.${service} = { + isSystemUser = true; + uid = cfg.services.${service}.user.id; + name = service; + group = service; + }; + groups.${service} = { + gid = cfg.services.${service}.group.id; + name = service; + }; + }; + mkServiceStateDir = service: dir: { + settings."${service}StateDir".${dir}."d" = { + mode = "0700"; + user = cfg.services.${service}.user.name; + group = cfg.services.${service}.group.name; + }; + }; +in { + inherit mkServiceOptions mkServiceStateDir mkServiceUser mkUserOption mkPortOption mkGroupOption mkVolumeOption mkSubdomainOption; +} diff --git a/modules/nixos/server/services/ookflix/options.nix b/modules/nixos/server/services/ookflix/options.nix new file mode 100644 index 0000000..7cd5550 --- /dev/null +++ b/modules/nixos/server/services/ookflix/options.nix @@ -0,0 +1,92 @@ +{ + lib, + config, + ... +}: let + ookflixLib = import ./lib.nix {inherit lib config;}; + + inherit (ookflixLib) mkVolumeOption mkGroupOption mkServiceOptions; + inherit (lib) mkOption mkEnableOption; + inherit (lib.types) enum; + inherit (config.ooknet) server; + cfg = server.ookflix; +in { + options.ooknet.server.ookflix = { + enable = mkEnableOption "Enable ookflix a container based media server module"; + gpuAcceleration = { + enable = mkEnableOption "Enable GPU acceleration for video streamers"; + type = mkOption { + type = enum ["nvidia" "intel" "amd"]; + default = config.ooknet.hardware.gpu.type; + description = '' + What GPU type to use for GPU acceleration. + Defaults to system GPU type (ooknet.hardware.gpu.type) + ''; + }; + }; + volumes = { + state.root = mkVolumeOption "root" "/var/lib/ookflix"; + content.root = mkVolumeOption "root" "/jellyfin"; + downloads = { + root = mkVolumeOption "${cfg.content.root}/downloads"; + incomplete = mkVolumeOption "downloads" "incomplete"; + complete = mkVolumeOption "downloads" "complete"; + watch = mkVolumeOption "downloads" "watch"; + }; + + media = { + root = mkVolumeOption "root" "${cfg.volumes.content.root}/media"; + movies = mkVolumeOption "media" "movies"; + tv = mkVolumeOption "media" "tv"; + }; + }; + # Shared groups + groups = { + media = mkGroupOption "media" 992; + downloader = mkGroupOption "downloader" 981; + }; + + services = { + jellyfin = mkServiceOptions "jellyfin" { + port = 8096; + uid = 994; + gid = 994; + }; + plex = mkServiceOptions "plex" { + port = 32400; + uid = 195; + gid = 195; + }; + sonarr = mkServiceOptions "sonarr" { + port = 8989; + uid = 274; + gid = 274; + }; + radarr = mkServiceOptions "radarr" { + port = 7878; + uid = 275; + gid = 275; + }; + prowlarr = mkServiceOptions "prowlarr" { + port = 9696; + uid = 982; + gid = 987; + }; + transmission = mkServiceOptions "transmission" { + port = 9091; + uid = 70; + gid = 70; + }; + jellyseer = mkServiceOptions "jellyseer" { + port = 5055; + uid = 345; + gid = 345; + }; + tautulli = mkServiceOptions "tautulli" { + port = 8181; + uid = 355; + gid = 355; + }; + }; + }; +} diff --git a/modules/nixos/server/services/ookflix/plex.nix b/modules/nixos/server/services/ookflix/plex.nix new file mode 100644 index 0000000..1bee37e --- /dev/null +++ b/modules/nixos/server/services/ookflix/plex.nix @@ -0,0 +1,66 @@ +{ + config, + lib, + ook, + ... +}: let + ookflixLib = import ./lib.nix {inherit lib config;}; + inherit (ookflixLib) mkServiceUser mkServiceStateDir; + inherit (lib) mkIf optionalAttrs; + inherit (ook.lib.container) mkContainerLabel mkContainerEnvironment mkContainerPort; + inherit (config.ooknet.server.ookflix) gpuAcceleration services volumes groups; + inherit (config.ooknet.server.ookflix.services) plex; +in { + config = mkIf plex.enable { + # not sure if this is needed for podman + hardware.nvidia-container-toolkit.enable = gpuAcceleration.enable && gpuAcceleration.type == "nvidia"; + + # users/group/directories configuration, see lib.nix + users = mkServiceUser plex.user.name; + systemd.tmpfiles = mkServiceStateDir "plex" plex.stateDir; + + # container configuration + virtualisation.oci-containers.containers = { + # media streaming server + plex = { + image = "lscr.io/linuxserver/plex:latest"; + autoStart = true; + hostname = "plex"; + ports = [(mkContainerPort plex.port)]; + volumes = [ + "${volumes.media.movies}:/data/movies" + "${volumes.media.tv}:/data/tv" + "${plex.stateDir}:/config" + ]; + labels = mkContainerLabel { + name = "plex"; + inherit (plex) domain port; + homepage = { + group = "media"; + description = "media-server streamer"; + }; + }; + extraOptions = optionalAttrs gpuAcceleration.enable ( + if gpuAcceleration.type == "nvidia" + then [ + "--runtime=nvidia" + ] + else if gpuAcceleration.type == "intel" + then [ + "--device=/dev/dri:/dev/dri" + ] + else if gpuAcceleration.type == "amd" + then [ + "--device=/dev/dri:/dev/dri" + ] + else [] + ); + environment = + mkContainerEnvironment plex.user.id groups.media.id + // optionalAttrs (gpuAcceleration.enable && gpuAcceleration.type == "nvidia") { + NVIDIA_VISIBLE_DEVICES = "all"; + }; + }; + }; + }; +} diff --git a/modules/nixos/server/services/ookflix/tautulli.nix b/modules/nixos/server/services/ookflix/tautulli.nix new file mode 100644 index 0000000..f99fad6 --- /dev/null +++ b/modules/nixos/server/services/ookflix/tautulli.nix @@ -0,0 +1,37 @@ +{ + config, + lib, + ook, + ... +}: let + ookflixLib = import ./lib.nix {inherit lib config;}; + inherit (ookflixLib) mkServiceUser mkServiceStateDir; + inherit (lib) mkIf; + inherit (ook.lib.container) mkContainerLabel mkContainerEnvironment mkContainerPort; + inherit (config.ooknet.server.ookflix) groups; + inherit (config.ooknet.server.services) tautulli; +in { + config = mkIf tautulli.enable { + users = mkServiceUser tautulli.user.name; + systemd.tmpfiles = mkServiceStateDir "tautulli" tautulli.stateDir; + virtualisation.oci-containers.containers = { + # plex monitoring service + tautulli = { + image = "lscr.io/linuxserver/tautulli:latest"; + autoStart = true; + hostname = "tautulli"; + ports = [(mkContainerPort tautulli.port)]; + volumes = ["${tautulli.stateDir}:/config"]; + labels = mkContainerLabel { + name = "tautulli"; + inherit (tautulli) port domain; + homepage = { + group = "monitoring"; + description = "media-server monitoring"; + }; + }; + environment = mkContainerEnvironment tautulli.user.id groups.media.id; + }; + }; + }; +} diff --git a/modules/nixos/server/services/ookflix/users.nix b/modules/nixos/server/services/ookflix/users.nix new file mode 100644 index 0000000..63790b0 --- /dev/null +++ b/modules/nixos/server/services/ookflix/users.nix @@ -0,0 +1,38 @@ +{ + lib, + config, + ... +}: let + inherit (lib) mkIf mkMerge; + inherit (builtins) mapAttrs; + inherit (config.ooknet.server) ookflix; + inherit (config.ooknet.server.ookflix) services storage users groups; + + mkServiceUser = name: user: { + isSystemUser = true; + group = groups.${name}.name; + uid = users.${name}.id; + home = storage.state.${name}; + }; + + generateUsers = mapAttrs mkServiceUser users; +in { + config = mkIf ookflix.enable { + users = { + users = mkMerge [ + # media service users + (mkIf services.jellyfin.enable { + ${users.jellyfin.name} = mkServiceUser users.jellyfin.name groups.media.name; + }) + (mkIf services.plex.enable { + ${users.plex.name} = mkServiceUser users.plex.name groups.media.name; + }) + (mkIf (services.jellyfin.enable || services.jellyseer.enable) { + ${users.jellyseer.name} = mkServiceUser users.jellyseer.name groups.media.name; + }) + ]; + groups = { + }; + }; + }; +} diff --git a/outputs/lib/containers.nix b/outputs/lib/containers.nix index 829f800..36c002d 100644 --- a/outputs/lib/containers.nix +++ b/outputs/lib/containers.nix @@ -1,4 +1,8 @@ -{lib, ...}: let +{ + lib, + config, + ... +}: let inherit (builtins) isBool; inherit (lib) toUpper optionalAttrs mapAttrs' nameValuePair; @@ -33,7 +37,7 @@ in commonLabels // (processWidget widget); - mkContainerLabels = {name, ...} @ args: let + mkContainerLabel = {name, ...} @ args: let homepage = args.homepage or {}; baseWidget = homepage.widget or {}; in @@ -52,16 +56,25 @@ # homepage labels // (optionalAttrs (args ? homepage) (mkHomepageLabels { inherit name; - inherit (args) domain; + domain = "https://${args.domain}"; group = args.homepage.group or name; widget = baseWidget // { type = name; - url = args.domain; + url = "https://${args.domain}"; key = "{{HOMEPAGE_FILE_${toUpper name}}}"; }; })); + + mkContainerEnvironment = user: group: { + PUID = toString user; + PGID = toString group; + # TODO: I dont want to hard code this + TZ = "Antarctica/Macquarie"; + }; + + mkContainerPort = port: "${toString port}:${toString port}"; in { - inherit mkContainerLabels; + inherit mkContainerLabel mkContainerEnvironment mkContainerPort; } diff --git a/outputs/lib/default.nix b/outputs/lib/default.nix index 42ee09f..4492913 100644 --- a/outputs/lib/default.nix +++ b/outputs/lib/default.nix @@ -2,6 +2,7 @@ lib, self, inputs, + config, ... }: let # my scuffed lib @@ -9,6 +10,7 @@ builders = import ./builders.nix {inherit self lib inputs;}; mkNeovim = import ./mkNeovim.nix {inherit inputs;}; math = import ./math.nix {inherit lib;}; + container = import ./containers.nix {inherit lib config;}; color = let check = import ./color/check.nix {inherit lib;}; types = import ./color/types.nix {