nixos/invidious: add option to run more invidious instances

+130 -79
+130 -79
nixos/modules/services/web-apps/invidious.nix
··· 10 generatedHmacKeyFile = "/var/lib/invidious/hmac_key"; 11 generateHmac = cfg.hmacKeyFile == null; 12 13 - serviceConfig = { 14 - systemd.services.invidious = { 15 - description = "Invidious (An alternative YouTube front-end)"; 16 - wants = [ "network-online.target" ]; 17 - after = [ "network-online.target" ]; 18 - wantedBy = [ "multi-user.target" ]; 19 20 - preStart = lib.optionalString generateHmac '' 21 - if [[ ! -e "${generatedHmacKeyFile}" ]]; then 22 - ${pkgs.pwgen}/bin/pwgen 20 1 > "${generatedHmacKeyFile}" 23 - chmod 0600 "${generatedHmacKeyFile}" 24 - fi 25 - ''; 26 27 - script = '' 28 - configParts=() 29 - '' 30 - # autogenerated hmac_key 31 - + lib.optionalString generateHmac '' 32 - configParts+=("$(${pkgs.jq}/bin/jq -R '{"hmac_key":.}' <"${generatedHmacKeyFile}")") 33 - '' 34 - # generated settings file 35 - + '' 36 - configParts+=("$(< ${lib.escapeShellArg settingsFile})") 37 - '' 38 - # optional database password file 39 - + lib.optionalString (cfg.database.host != null) '' 40 - configParts+=("$(${pkgs.jq}/bin/jq -R '{"db":{"password":.}}' ${lib.escapeShellArg cfg.database.passwordFile})") 41 - '' 42 - # optional extra settings file 43 - + lib.optionalString (cfg.extraSettingsFile != null) '' 44 - configParts+=("$(< ${lib.escapeShellArg cfg.extraSettingsFile})") 45 - '' 46 - # explicitly specified hmac key file 47 - + lib.optionalString (cfg.hmacKeyFile != null) '' 48 - configParts+=("$(< ${lib.escapeShellArg cfg.hmacKeyFile})") 49 - '' 50 - # merge all parts into a single configuration with later elements overriding previous elements 51 - + '' 52 - export INVIDIOUS_CONFIG="$(${pkgs.jq}/bin/jq -s 'reduce .[] as $item ({}; . * $item)' <<<"''${configParts[*]}")" 53 - exec ${cfg.package}/bin/invidious 54 - ''; 55 56 - serviceConfig = { 57 - RestartSec = "2s"; 58 - DynamicUser = true; 59 - StateDirectory = "invidious"; 60 - StateDirectoryMode = "0750"; 61 - 62 - CapabilityBoundingSet = ""; 63 - PrivateDevices = true; 64 - PrivateUsers = true; 65 - ProtectHome = true; 66 - ProtectKernelLogs = true; 67 - ProtectProc = "invisible"; 68 - RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; 69 - RestrictNamespaces = true; 70 - SystemCallArchitectures = "native"; 71 - SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ]; 72 73 - # Because of various issues Invidious must be restarted often, at least once a day, ideally 74 - # every hour. 75 - # This option enables the automatic restarting of the Invidious instance. 76 - Restart = lib.mkDefault "always"; 77 - RuntimeMaxSec = lib.mkDefault "1h"; 78 - }; 79 - }; 80 81 services.invidious.settings = { 82 - inherit (cfg) port; 83 - 84 # Automatically initialises and migrates the database if necessary 85 check_tables = true; 86 ··· 98 inherit (cfg) domain; 99 }); 100 101 - assertions = [{ 102 - assertion = cfg.database.host != null -> cfg.database.passwordFile != null; 103 - message = "If database host isn't null, database password needs to be set"; 104 - }]; 105 }; 106 107 # Settings necessary for running with an automatically managed local database ··· 132 local ${cfg.settings.db.dbname} ${cfg.settings.db.user} peer map=invidious 133 ''; 134 }; 135 - 136 - systemd.services.invidious = { 137 - requires = [ "postgresql.service" ]; 138 - after = [ "postgresql.service" ]; 139 - 140 - serviceConfig = { 141 - User = "invidious"; 142 - }; 143 - }; 144 }; 145 146 nginxConfig = lib.mkIf cfg.nginx.enable { ··· 152 services.nginx = { 153 enable = true; 154 virtualHosts.${cfg.domain} = { 155 - locations."/".proxyPass = "http://127.0.0.1:${toString cfg.port}"; 156 157 enableACME = lib.mkDefault true; 158 forceSSL = lib.mkDefault true; 159 }; 160 }; 161 ··· 201 202 It gets merged with the settings specified in {option}`services.invidious.settings` 203 and can be used to store secrets like `hmac_key` outside of the nix store. 204 ''; 205 }; 206
··· 10 generatedHmacKeyFile = "/var/lib/invidious/hmac_key"; 11 generateHmac = cfg.hmacKeyFile == null; 12 13 + commonInvidousServiceConfig = { 14 + description = "Invidious (An alternative YouTube front-end)"; 15 + wants = [ "network-online.target" ]; 16 + after = [ "network-online.target" ] ++ lib.optional cfg.database.createLocally "postgresql.service"; 17 + requires = lib.optional cfg.database.createLocally "postgresql.service"; 18 + wantedBy = [ "multi-user.target" ]; 19 20 + serviceConfig = { 21 + RestartSec = "2s"; 22 + DynamicUser = true; 23 + User = lib.mkIf (cfg.database.createLocally || cfg.serviceScale > 1) "invidious"; 24 + StateDirectory = "invidious"; 25 + StateDirectoryMode = "0750"; 26 27 + CapabilityBoundingSet = ""; 28 + PrivateDevices = true; 29 + PrivateUsers = true; 30 + ProtectHome = true; 31 + ProtectKernelLogs = true; 32 + ProtectProc = "invisible"; 33 + RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; 34 + RestrictNamespaces = true; 35 + SystemCallArchitectures = "native"; 36 + SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ]; 37 38 + # Because of various issues Invidious must be restarted often, at least once a day, ideally 39 + # every hour. 40 + # This option enables the automatic restarting of the Invidious instance. 41 + # To ensure multiple instances of Invidious are not restarted at the exact same time, a 42 + # randomized extra offset of up to 5 minutes is added. 43 + Restart = lib.mkDefault "always"; 44 + RuntimeMaxSec = lib.mkDefault "1h"; 45 + RuntimeRandomizedExtraSec = lib.mkDefault "5min"; 46 + }; 47 + }; 48 + mkInvidiousService = scaleIndex: 49 + lib.foldl' lib.recursiveUpdate commonInvidousServiceConfig [ 50 + # only generate the hmac file in the first service 51 + (lib.optionalAttrs (scaleIndex == 0) { 52 + preStart = lib.optionalString generateHmac '' 53 + if [[ ! -e "${generatedHmacKeyFile}" ]]; then 54 + ${pkgs.pwgen}/bin/pwgen 20 1 > "${generatedHmacKeyFile}" 55 + chmod 0600 "${generatedHmacKeyFile}" 56 + fi 57 + ''; 58 + }) 59 + # configure the secondary services to run after the first service 60 + (lib.optionalAttrs (scaleIndex > 0) { 61 + after = commonInvidousServiceConfig.after ++ [ "invidious.service" ]; 62 + wants = commonInvidousServiceConfig.wants ++ [ "invidious.service" ]; 63 + }) 64 + { 65 + script = '' 66 + configParts=() 67 + '' 68 + # autogenerated hmac_key 69 + + lib.optionalString generateHmac '' 70 + configParts+=("$(${pkgs.jq}/bin/jq -R '{"hmac_key":.}' <"${generatedHmacKeyFile}")") 71 + '' 72 + # generated settings file 73 + + '' 74 + configParts+=("$(< ${lib.escapeShellArg settingsFile})") 75 + '' 76 + # optional database password file 77 + + lib.optionalString (cfg.database.host != null) '' 78 + configParts+=("$(${pkgs.jq}/bin/jq -R '{"db":{"password":.}}' ${lib.escapeShellArg cfg.database.passwordFile})") 79 + '' 80 + # optional extra settings file 81 + + lib.optionalString (cfg.extraSettingsFile != null) '' 82 + configParts+=("$(< ${lib.escapeShellArg cfg.extraSettingsFile})") 83 + '' 84 + # explicitly specified hmac key file 85 + + lib.optionalString (cfg.hmacKeyFile != null) '' 86 + configParts+=("$(< ${lib.escapeShellArg cfg.hmacKeyFile})") 87 + '' 88 + # configure threads for secondary instances 89 + + lib.optionalString (scaleIndex > 0) '' 90 + configParts+=('{"channel_threads":0, "feed_threads":0}') 91 + '' 92 + # configure different ports for the instances 93 + + '' 94 + configParts+=('{"port":${toString (cfg.port + scaleIndex)}}') 95 + '' 96 + # merge all parts into a single configuration with later elements overriding previous elements 97 + + '' 98 + export INVIDIOUS_CONFIG="$(${pkgs.jq}/bin/jq -s 'reduce .[] as $item ({}; . * $item)' <<<"''${configParts[*]}")" 99 + exec ${cfg.package}/bin/invidious 100 + ''; 101 + } 102 + ]; 103 104 + serviceConfig = { 105 + systemd.services = builtins.listToAttrs (builtins.genList 106 + (scaleIndex: { 107 + name = "invidious" + lib.optionalString (scaleIndex > 0) "-${builtins.toString scaleIndex}"; 108 + value = mkInvidiousService scaleIndex; 109 + }) 110 + cfg.serviceScale); 111 112 services.invidious.settings = { 113 # Automatically initialises and migrates the database if necessary 114 check_tables = true; 115 ··· 127 inherit (cfg) domain; 128 }); 129 130 + assertions = [ 131 + { 132 + assertion = cfg.database.host != null -> cfg.database.passwordFile != null; 133 + message = "If database host isn't null, database password needs to be set"; 134 + } 135 + { 136 + assertion = cfg.serviceScale >= 1; 137 + message = "Service can't be scaled below one instance"; 138 + } 139 + ]; 140 }; 141 142 # Settings necessary for running with an automatically managed local database ··· 167 local ${cfg.settings.db.dbname} ${cfg.settings.db.user} peer map=invidious 168 ''; 169 }; 170 }; 171 172 nginxConfig = lib.mkIf cfg.nginx.enable { ··· 178 services.nginx = { 179 enable = true; 180 virtualHosts.${cfg.domain} = { 181 + locations."/".proxyPass = 182 + if cfg.serviceScale == 1 then 183 + "http://127.0.0.1:${toString cfg.port}" 184 + else "http://upstream-invidious"; 185 186 enableACME = lib.mkDefault true; 187 forceSSL = lib.mkDefault true; 188 + }; 189 + upstreams = lib.mkIf (cfg.serviceScale > 1) { 190 + "upstream-invidious".servers = builtins.listToAttrs (builtins.genList 191 + (scaleIndex: { 192 + name = "127.0.0.1:${toString (cfg.port + scaleIndex)}"; 193 + value = { }; 194 + }) 195 + cfg.serviceScale); 196 }; 197 }; 198 ··· 238 239 It gets merged with the settings specified in {option}`services.invidious.settings` 240 and can be used to store secrets like `hmac_key` outside of the nix store. 241 + ''; 242 + }; 243 + 244 + serviceScale = lib.mkOption { 245 + type = types.int; 246 + default = 1; 247 + description = lib.mdDoc '' 248 + How many invidious instances to run. 249 + 250 + See https://docs.invidious.io/improve-public-instance/#2-multiple-invidious-processes for more details 251 + on how this is intended to work. All instances beyond the first one have the options `channel_threads` 252 + and `feed_threads` set to 0 to avoid conflicts with multiple instances refreshing subscriptions. Instances 253 + will be configured to bind to consecutive ports starting with {option}`services.invidious.port` for the 254 + first instance. 255 ''; 256 }; 257