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