at 25.11-pre 17 kB view raw
1{ 2 lib, 3 config, 4 pkgs, 5 options, 6 ... 7}: 8let 9 cfg = config.services.invidious; 10 # To allow injecting secrets with jq, json (instead of yaml) is used 11 settingsFormat = pkgs.formats.json { }; 12 inherit (lib) types; 13 14 settingsFile = settingsFormat.generate "invidious-settings" cfg.settings; 15 16 generatedHmacKeyFile = "/var/lib/invidious/hmac_key"; 17 generateHmac = cfg.hmacKeyFile == null; 18 19 commonInvidousServiceConfig = { 20 description = "Invidious (An alternative YouTube front-end)"; 21 wants = [ "network-online.target" ]; 22 after = [ "network-online.target" ] ++ lib.optional cfg.database.createLocally "postgresql.service"; 23 requires = lib.optional cfg.database.createLocally "postgresql.service"; 24 wantedBy = [ "multi-user.target" ]; 25 26 serviceConfig = { 27 RestartSec = "2s"; 28 DynamicUser = true; 29 User = lib.mkIf (cfg.database.createLocally || cfg.serviceScale > 1) "invidious"; 30 StateDirectory = "invidious"; 31 StateDirectoryMode = "0750"; 32 33 CapabilityBoundingSet = ""; 34 PrivateDevices = true; 35 PrivateUsers = true; 36 ProtectHome = true; 37 ProtectKernelLogs = true; 38 ProtectProc = "invisible"; 39 RestrictAddressFamilies = [ 40 "AF_UNIX" 41 "AF_INET" 42 "AF_INET6" 43 ]; 44 RestrictNamespaces = true; 45 SystemCallArchitectures = "native"; 46 SystemCallFilter = [ 47 "@system-service" 48 "~@privileged" 49 "~@resources" 50 ]; 51 52 # Because of various issues Invidious must be restarted often, at least once a day, ideally 53 # every hour. 54 # This option enables the automatic restarting of the Invidious instance. 55 # To ensure multiple instances of Invidious are not restarted at the exact same time, a 56 # randomized extra offset of up to 5 minutes is added. 57 Restart = lib.mkDefault "always"; 58 RuntimeMaxSec = lib.mkDefault "1h"; 59 RuntimeRandomizedExtraSec = lib.mkDefault "5min"; 60 }; 61 }; 62 mkInvidiousService = 63 scaleIndex: 64 lib.foldl' lib.recursiveUpdate commonInvidousServiceConfig [ 65 # only generate the hmac file in the first service 66 (lib.optionalAttrs (scaleIndex == 0) { 67 preStart = lib.optionalString generateHmac '' 68 if [[ ! -e "${generatedHmacKeyFile}" ]]; then 69 ${pkgs.pwgen}/bin/pwgen 20 1 > "${generatedHmacKeyFile}" 70 chmod 0600 "${generatedHmacKeyFile}" 71 fi 72 ''; 73 }) 74 # configure the secondary services to run after the first service 75 (lib.optionalAttrs (scaleIndex > 0) { 76 after = commonInvidousServiceConfig.after ++ [ "invidious.service" ]; 77 wants = commonInvidousServiceConfig.wants ++ [ "invidious.service" ]; 78 }) 79 { 80 script = 81 '' 82 configParts=() 83 '' 84 # autogenerated hmac_key 85 + lib.optionalString generateHmac '' 86 configParts+=("$(${pkgs.jq}/bin/jq -R '{"hmac_key":.}' <"${generatedHmacKeyFile}")") 87 '' 88 # generated settings file 89 + '' 90 configParts+=("$(< ${lib.escapeShellArg settingsFile})") 91 '' 92 # optional database password file 93 + lib.optionalString (cfg.database.host != null) '' 94 configParts+=("$(${pkgs.jq}/bin/jq -R '{"db":{"password":.}}' ${lib.escapeShellArg cfg.database.passwordFile})") 95 '' 96 # optional extra settings file 97 + lib.optionalString (cfg.extraSettingsFile != null) '' 98 configParts+=("$(< ${lib.escapeShellArg cfg.extraSettingsFile})") 99 '' 100 # explicitly specified hmac key file 101 + lib.optionalString (cfg.hmacKeyFile != null) '' 102 configParts+=("$(< ${lib.escapeShellArg cfg.hmacKeyFile})") 103 '' 104 # configure threads for secondary instances 105 + lib.optionalString (scaleIndex > 0) '' 106 configParts+=('{"channel_threads":0, "feed_threads":0}') 107 '' 108 # configure different ports for the instances 109 + '' 110 configParts+=('{"port":${toString (cfg.port + scaleIndex)}}') 111 '' 112 # merge all parts into a single configuration with later elements overriding previous elements 113 + '' 114 export INVIDIOUS_CONFIG="$(${pkgs.jq}/bin/jq -s 'reduce .[] as $item ({}; . * $item)' <<<"''${configParts[*]}")" 115 exec ${cfg.package}/bin/invidious 116 ''; 117 } 118 ]; 119 120 serviceConfig = { 121 systemd.services = builtins.listToAttrs ( 122 builtins.genList (scaleIndex: { 123 name = "invidious" + lib.optionalString (scaleIndex > 0) "-${builtins.toString scaleIndex}"; 124 value = mkInvidiousService scaleIndex; 125 }) cfg.serviceScale 126 ); 127 128 services.invidious.settings = 129 { 130 # Automatically initialises and migrates the database if necessary 131 check_tables = true; 132 133 db = { 134 user = lib.mkDefault ( 135 if (lib.versionAtLeast config.system.stateVersion "24.05") then "invidious" else "kemal" 136 ); 137 dbname = lib.mkDefault "invidious"; 138 port = cfg.database.port; 139 # Blank for unix sockets, see 140 # https://github.com/will/crystal-pg/blob/1548bb255210/src/pq/conninfo.cr#L100-L108 141 host = lib.optionalString (cfg.database.host != null) cfg.database.host; 142 # Not needed because peer authentication is enabled 143 password = lib.mkIf (cfg.database.host == null) ""; 144 }; 145 146 host_binding = cfg.address; 147 } 148 // (lib.optionalAttrs (cfg.domain != null) { 149 inherit (cfg) domain; 150 }); 151 152 assertions = [ 153 { 154 assertion = cfg.database.host != null -> cfg.database.passwordFile != null; 155 message = "If database host isn't null, database password needs to be set"; 156 } 157 { 158 assertion = cfg.serviceScale >= 1; 159 message = "Service can't be scaled below one instance"; 160 } 161 ]; 162 }; 163 164 # Settings necessary for running with an automatically managed local database 165 localDatabaseConfig = lib.mkIf cfg.database.createLocally { 166 assertions = [ 167 { 168 assertion = cfg.settings.db.user == cfg.settings.db.dbname; 169 message = '' 170 For local automatic database provisioning (services.invidious.database.createLocally == true) 171 to work, the username used to connect to PostgreSQL must match the database name, that is 172 services.invidious.settings.db.user must match services.invidious.settings.db.dbname. 173 This is the default since NixOS 24.05. For older systems, it is normally safe to manually set 174 the user to "invidious" as the new user will be created with permissions 175 for the existing database. `REASSIGN OWNED BY kemal TO invidious;` may also be needed, it can be 176 run as `sudo -u postgres env psql --user=postgres --dbname=invidious -c 'reassign OWNED BY kemal to invidious;'`. 177 ''; 178 } 179 ]; 180 # Default to using the local database if we create it 181 services.invidious.database.host = lib.mkDefault null; 182 183 services.postgresql = { 184 enable = true; 185 ensureUsers = lib.singleton { 186 name = cfg.settings.db.user; 187 ensureDBOwnership = true; 188 }; 189 ensureDatabases = lib.singleton cfg.settings.db.dbname; 190 }; 191 }; 192 193 ytproxyConfig = lib.mkIf cfg.http3-ytproxy.enable { 194 systemd.services.http3-ytproxy = { 195 description = "HTTP3 ytproxy for Invidious"; 196 wants = [ "network-online.target" ]; 197 after = [ "network-online.target" ]; 198 wantedBy = [ "multi-user.target" ]; 199 200 script = '' 201 mkdir -p socket 202 exec ${lib.getExe cfg.http3-ytproxy.package}; 203 ''; 204 205 serviceConfig = { 206 RestartSec = "2s"; 207 DynamicUser = true; 208 User = lib.mkIf cfg.nginx.enable config.services.nginx.user; 209 RuntimeDirectory = "http3-ytproxy"; 210 WorkingDirectory = "/run/http3-ytproxy"; 211 }; 212 }; 213 214 services.nginx.virtualHosts.${cfg.domain} = lib.mkIf cfg.nginx.enable { 215 locations."~ (^/videoplayback|^/vi/|^/ggpht/|^/sb/)" = { 216 proxyPass = "http://unix:/run/http3-ytproxy/socket/http-proxy.sock"; 217 }; 218 }; 219 }; 220 221 sigHelperConfig = lib.mkIf cfg.sig-helper.enable { 222 services.invidious.settings.signature_server = "tcp://${cfg.sig-helper.listenAddress}"; 223 systemd.services.invidious-sig-helper = { 224 script = '' 225 exec ${lib.getExe cfg.sig-helper.package} --tcp "${cfg.sig-helper.listenAddress}" 226 ''; 227 wantedBy = [ "multi-user.target" ]; 228 before = [ "invidious.service" ]; 229 wants = [ "network-online.target" ]; 230 after = [ "network-online.target" ]; 231 serviceConfig = { 232 User = "invidious-sig-helper"; 233 DynamicUser = true; 234 Restart = "always"; 235 236 PrivateTmp = true; 237 PrivateUsers = true; 238 ProtectSystem = true; 239 ProtectProc = "invisible"; 240 ProtectHome = true; 241 PrivateDevices = true; 242 NoNewPrivileges = true; 243 ProtectKernelTunables = true; 244 ProtectKernelModules = true; 245 ProtectControlGroups = true; 246 ProtectKernelLogs = true; 247 CapabilityBoundingSet = ""; 248 SystemCallArchitectures = "native"; 249 SystemCallFilter = [ 250 "@system-service" 251 "~@privileged" 252 "~@resources" 253 "@network-io" 254 ]; 255 RestrictAddressFamilies = [ 256 "AF_INET" 257 "AF_INET6" 258 ]; 259 RestrictNamespaces = true; 260 }; 261 }; 262 }; 263 264 nginxConfig = lib.mkIf cfg.nginx.enable { 265 services.invidious.settings = { 266 https_only = config.services.nginx.virtualHosts.${cfg.domain}.forceSSL; 267 external_port = 80; 268 }; 269 270 services.nginx = 271 let 272 ip = if cfg.address == "0.0.0.0" then "127.0.0.1" else cfg.address; 273 in 274 { 275 enable = true; 276 virtualHosts.${cfg.domain} = { 277 locations."/".proxyPass = 278 if cfg.serviceScale == 1 then "http://${ip}:${toString cfg.port}" else "http://upstream-invidious"; 279 280 enableACME = lib.mkDefault true; 281 forceSSL = lib.mkDefault true; 282 }; 283 upstreams = lib.mkIf (cfg.serviceScale > 1) { 284 "upstream-invidious".servers = builtins.listToAttrs ( 285 builtins.genList (scaleIndex: { 286 name = "${ip}:${toString (cfg.port + scaleIndex)}"; 287 value = { }; 288 }) cfg.serviceScale 289 ); 290 }; 291 }; 292 293 assertions = [ 294 { 295 assertion = cfg.domain != null; 296 message = "To use services.invidious.nginx, you need to set services.invidious.domain"; 297 } 298 ]; 299 }; 300in 301{ 302 options.services.invidious = { 303 enable = lib.mkEnableOption "Invidious"; 304 305 package = lib.mkPackageOption pkgs "invidious" { }; 306 307 settings = lib.mkOption { 308 type = settingsFormat.type; 309 default = { }; 310 description = '' 311 The settings Invidious should use. 312 313 See [config.example.yml](https://github.com/iv-org/invidious/blob/master/config/config.example.yml) for a list of all possible options. 314 ''; 315 }; 316 317 hmacKeyFile = lib.mkOption { 318 type = types.nullOr types.path; 319 default = null; 320 description = '' 321 A path to a file containing the `hmac_key`. If `null`, a key will be generated automatically on first 322 start. 323 324 If non-`null`, this option overrides any `hmac_key` specified in {option}`services.invidious.settings` or 325 via {option}`services.invidious.extraSettingsFile`. 326 ''; 327 }; 328 329 extraSettingsFile = lib.mkOption { 330 type = types.nullOr types.str; 331 default = null; 332 description = '' 333 A file including Invidious settings. 334 335 It gets merged with the settings specified in {option}`services.invidious.settings` 336 and can be used to store secrets like `hmac_key` outside of the nix store. 337 ''; 338 }; 339 340 serviceScale = lib.mkOption { 341 type = types.int; 342 default = 1; 343 description = '' 344 How many invidious instances to run. 345 346 See <https://docs.invidious.io/improve-public-instance/#2-multiple-invidious-processes> for more details 347 on how this is intended to work. All instances beyond the first one have the options `channel_threads` 348 and `feed_threads` set to 0 to avoid conflicts with multiple instances refreshing subscriptions. Instances 349 will be configured to bind to consecutive ports starting with {option}`services.invidious.port` for the 350 first instance. 351 ''; 352 }; 353 354 # This needs to be outside of settings to avoid infinite recursion 355 # (determining if nginx should be enabled and therefore the settings 356 # modified). 357 domain = lib.mkOption { 358 type = types.nullOr types.str; 359 default = null; 360 description = '' 361 The FQDN Invidious is reachable on. 362 363 This is used to configure nginx and for building absolute URLs. 364 ''; 365 }; 366 367 address = lib.mkOption { 368 type = types.str; 369 # default from https://github.com/iv-org/invidious/blob/master/config/config.example.yml 370 default = if cfg.nginx.enable then "127.0.0.1" else "0.0.0.0"; 371 defaultText = lib.literalExpression ''if config.services.invidious.nginx.enable then "127.0.0.1" else "0.0.0.0"''; 372 description = '' 373 The IP address Invidious should bind to. 374 ''; 375 }; 376 377 port = lib.mkOption { 378 type = types.port; 379 # Default from https://docs.invidious.io/Configuration.md 380 default = 3000; 381 description = '' 382 The port Invidious should listen on. 383 384 To allow access from outside, 385 you can use either {option}`services.invidious.nginx` 386 or add `config.services.invidious.port` to {option}`networking.firewall.allowedTCPPorts`. 387 ''; 388 }; 389 390 database = { 391 createLocally = lib.mkOption { 392 type = types.bool; 393 default = true; 394 description = '' 395 Whether to create a local database with PostgreSQL. 396 ''; 397 }; 398 399 host = lib.mkOption { 400 type = types.nullOr types.str; 401 default = null; 402 description = '' 403 The database host Invidious should use. 404 405 If `null`, the local unix socket is used. Otherwise 406 TCP is used. 407 ''; 408 }; 409 410 port = lib.mkOption { 411 type = types.port; 412 default = config.services.postgresql.settings.port; 413 defaultText = lib.literalExpression "config.services.postgresql.settings.port"; 414 description = '' 415 The port of the database Invidious should use. 416 417 Defaults to the the default postgresql port. 418 ''; 419 }; 420 421 passwordFile = lib.mkOption { 422 type = types.nullOr types.str; 423 apply = lib.mapNullable toString; 424 default = null; 425 description = '' 426 Path to file containing the database password. 427 ''; 428 }; 429 }; 430 431 nginx.enable = lib.mkOption { 432 type = types.bool; 433 default = false; 434 description = '' 435 Whether to configure nginx as a reverse proxy for Invidious. 436 437 It serves it under the domain specified in {option}`services.invidious.settings.domain` with enabled TLS and ACME. 438 Further configuration can be done through {option}`services.nginx.virtualHosts.''${config.services.invidious.settings.domain}.*`, 439 which can also be used to disable AMCE and TLS. 440 ''; 441 }; 442 443 http3-ytproxy = { 444 enable = lib.mkOption { 445 type = lib.types.bool; 446 default = false; 447 description = '' 448 Whether to enable http3-ytproxy for faster loading of images and video playback. 449 450 If {option}`services.invidious.nginx.enable` is used, nginx will be configured automatically. If not, you 451 need to configure a reverse proxy yourself according to 452 <https://docs.invidious.io/improve-public-instance/#3-speed-up-video-playback-with-http3-ytproxy>. 453 ''; 454 }; 455 456 package = lib.mkPackageOption pkgs "http3-ytproxy" { }; 457 }; 458 459 sig-helper = { 460 enable = lib.mkOption { 461 type = lib.types.bool; 462 default = false; 463 description = '' 464 Whether to enable and configure inv-sig-helper to emulate the youtube client's javascript. This is required 465 to make certain videos playable. 466 467 This will download and run completely untrusted javascript from youtube! While this service is sandboxed, 468 this may still be an issue! 469 ''; 470 }; 471 472 package = lib.mkPackageOption pkgs "inv-sig-helper" { }; 473 474 listenAddress = lib.mkOption { 475 type = lib.types.str; 476 default = "127.0.0.1:2999"; 477 description = '' 478 The IP address/port where inv-sig-helper should listen. 479 ''; 480 }; 481 }; 482 }; 483 484 config = lib.mkIf cfg.enable ( 485 lib.mkMerge [ 486 serviceConfig 487 localDatabaseConfig 488 nginxConfig 489 ytproxyConfig 490 sigHelperConfig 491 ] 492 ); 493}