1 { lib, config, pkgs, options, ... }:
3 cfg = config.services.invidious;
4 # To allow injecting secrets with jq, json (instead of yaml) is used
5 settingsFormat = pkgs.formats.json { };
8 settingsFile = settingsFormat.generate "invidious-settings" cfg.settings;
10 generatedHmacKeyFile = "/var/lib/invidious/hmac_key";
11 generateHmac = cfg.hmacKeyFile == null;
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" ];
23 User = lib.mkIf (cfg.database.createLocally || cfg.serviceScale > 1) "invidious";
24 StateDirectory = "invidious";
25 StateDirectoryMode = "0750";
27 CapabilityBoundingSet = "";
28 PrivateDevices = 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" ];
38 # Because of various issues Invidious must be restarted often, at least once a day, ideally
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";
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}"
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" ];
68 # autogenerated hmac_key
69 + lib.optionalString generateHmac ''
70 configParts+=("$(${pkgs.jq}/bin/jq -R '{"hmac_key":.}' <"${generatedHmacKeyFile}")")
72 # generated settings file
74 configParts+=("$(< ${lib.escapeShellArg settingsFile})")
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})")
80 # optional extra settings file
81 + lib.optionalString (cfg.extraSettingsFile != null) ''
82 configParts+=("$(< ${lib.escapeShellArg cfg.extraSettingsFile})")
84 # explicitly specified hmac key file
85 + lib.optionalString (cfg.hmacKeyFile != null) ''
86 configParts+=("$(< ${lib.escapeShellArg cfg.hmacKeyFile})")
88 # configure threads for secondary instances
89 + lib.optionalString (scaleIndex > 0) ''
90 configParts+=('{"channel_threads":0, "feed_threads":0}')
92 # configure different ports for the instances
94 configParts+=('{"port":${toString (cfg.port + scaleIndex)}}')
96 # merge all parts into a single configuration with later elements overriding previous elements
98 export INVIDIOUS_CONFIG="$(${pkgs.jq}/bin/jq -s 'reduce .[] as $item ({}; . * $item)' <<<"''${configParts[*]}")"
99 exec ${cfg.package}/bin/invidious
105 systemd.services = builtins.listToAttrs (builtins.genList
107 name = "invidious" + lib.optionalString (scaleIndex > 0) "-${builtins.toString scaleIndex}";
108 value = mkInvidiousService scaleIndex;
112 services.invidious.settings = {
113 # Automatically initialises and migrates the database if necessary
117 user = lib.mkDefault (
118 if (lib.versionAtLeast config.system.stateVersion "24.05")
122 dbname = lib.mkDefault "invidious";
123 port = cfg.database.port;
124 # Blank for unix sockets, see
125 # https://github.com/will/crystal-pg/blob/1548bb255210/src/pq/conninfo.cr#L100-L108
126 host = lib.optionalString (cfg.database.host != null) cfg.database.host;
127 # Not needed because peer authentication is enabled
128 password = lib.mkIf (cfg.database.host == null) "";
131 host_binding = cfg.address;
132 } // (lib.optionalAttrs (cfg.domain != null) {
133 inherit (cfg) domain;
138 assertion = cfg.database.host != null -> cfg.database.passwordFile != null;
139 message = "If database host isn't null, database password needs to be set";
142 assertion = cfg.serviceScale >= 1;
143 message = "Service can't be scaled below one instance";
148 # Settings necessary for running with an automatically managed local database
149 localDatabaseConfig = lib.mkIf cfg.database.createLocally {
152 assertion = cfg.settings.db.user == cfg.settings.db.dbname;
154 For local automatic database provisioning (services.invidious.database.createLocally == true)
155 to work, the username used to connect to PostgreSQL must match the database name, that is
156 services.invidious.settings.db.user must match services.invidious.settings.db.dbname.
157 This is the default since NixOS 24.05. For older systems, it is normally safe to manually set
158 the user to "invidious" as the new user will be created with permissions
159 for the existing database. `REASSIGN OWNED BY kemal TO invidious;` may also be needed, it can be
160 run as `sudo -u postgres env psql --user=postgres --dbname=invidious -c 'reassign OWNED BY kemal to invidious;'`.
164 # Default to using the local database if we create it
165 services.invidious.database.host = lib.mkDefault null;
167 services.postgresql = {
169 ensureUsers = lib.singleton { name = cfg.settings.db.user; ensureDBOwnership = true; };
170 ensureDatabases = lib.singleton cfg.settings.db.dbname;
174 ytproxyConfig = lib.mkIf cfg.http3-ytproxy.enable {
175 systemd.services.http3-ytproxy = {
176 description = "HTTP3 ytproxy for Invidious";
177 wants = [ "network-online.target" ];
178 after = [ "network-online.target" ];
179 wantedBy = [ "multi-user.target" ];
183 exec ${lib.getExe cfg.http3-ytproxy.package};
189 User = lib.mkIf cfg.nginx.enable config.services.nginx.user;
190 RuntimeDirectory = "http3-ytproxy";
191 WorkingDirectory = "/run/http3-ytproxy";
195 services.nginx.virtualHosts.${cfg.domain} = lib.mkIf cfg.nginx.enable {
196 locations."~ (^/videoplayback|^/vi/|^/ggpht/|^/sb/)" = {
197 proxyPass = "http://unix:/run/http3-ytproxy/socket/http-proxy.sock";
202 sigHelperConfig = lib.mkIf cfg.sig-helper.enable {
203 services.invidious.settings.signature_server = "tcp://${cfg.sig-helper.listenAddress}";
204 systemd.services.invidious-sig-helper = {
206 exec ${lib.getExe cfg.sig-helper.package} --tcp "${cfg.sig-helper.listenAddress}"
208 wantedBy = [ "multi-user.target" ];
209 before = [ "invidious.service" ];
210 wants = [ "network-online.target" ];
211 after = [ "network-online.target" ];
213 User = "invidious-sig-helper";
219 ProtectSystem = true;
220 ProtectProc = "invisible";
222 PrivateDevices = true;
223 NoNewPrivileges = true;
224 ProtectKernelTunables = true;
225 ProtectKernelModules = true;
226 ProtectControlGroups = true;
227 ProtectKernelLogs = true;
228 CapabilityBoundingSet = "";
229 SystemCallArchitectures = "native";
230 SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" "@network-io" ];
231 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
232 RestrictNamespaces = true;
237 nginxConfig = lib.mkIf cfg.nginx.enable {
238 services.invidious.settings = {
239 https_only = config.services.nginx.virtualHosts.${cfg.domain}.forceSSL;
244 ip = if cfg.address == "0.0.0.0" then "127.0.0.1" else cfg.address;
248 virtualHosts.${cfg.domain} = {
249 locations."/".proxyPass =
250 if cfg.serviceScale == 1 then
251 "http://${ip}:${toString cfg.port}"
252 else "http://upstream-invidious";
254 enableACME = lib.mkDefault true;
255 forceSSL = lib.mkDefault true;
257 upstreams = lib.mkIf (cfg.serviceScale > 1) {
258 "upstream-invidious".servers = builtins.listToAttrs (builtins.genList
260 name = "${ip}:${toString (cfg.port + scaleIndex)}";
268 assertion = cfg.domain != null;
269 message = "To use services.invidious.nginx, you need to set services.invidious.domain";
274 options.services.invidious = {
275 enable = lib.mkEnableOption "Invidious";
277 package = lib.mkPackageOption pkgs "invidious" { };
279 settings = lib.mkOption {
280 type = settingsFormat.type;
283 The settings Invidious should use.
285 See [config.example.yml](https://github.com/iv-org/invidious/blob/master/config/config.example.yml) for a list of all possible options.
289 hmacKeyFile = lib.mkOption {
290 type = types.nullOr types.path;
293 A path to a file containing the `hmac_key`. If `null`, a key will be generated automatically on first
296 If non-`null`, this option overrides any `hmac_key` specified in {option}`services.invidious.settings` or
297 via {option}`services.invidious.extraSettingsFile`.
301 extraSettingsFile = lib.mkOption {
302 type = types.nullOr types.str;
305 A file including Invidious settings.
307 It gets merged with the settings specified in {option}`services.invidious.settings`
308 and can be used to store secrets like `hmac_key` outside of the nix store.
312 serviceScale = lib.mkOption {
316 How many invidious instances to run.
318 See https://docs.invidious.io/improve-public-instance/#2-multiple-invidious-processes for more details
319 on how this is intended to work. All instances beyond the first one have the options `channel_threads`
320 and `feed_threads` set to 0 to avoid conflicts with multiple instances refreshing subscriptions. Instances
321 will be configured to bind to consecutive ports starting with {option}`services.invidious.port` for the
326 # This needs to be outside of settings to avoid infinite recursion
327 # (determining if nginx should be enabled and therefore the settings
329 domain = lib.mkOption {
330 type = types.nullOr types.str;
333 The FQDN Invidious is reachable on.
335 This is used to configure nginx and for building absolute URLs.
339 address = lib.mkOption {
341 # default from https://github.com/iv-org/invidious/blob/master/config/config.example.yml
342 default = if cfg.nginx.enable then "127.0.0.1" else "0.0.0.0";
343 defaultText = lib.literalExpression ''if config.services.invidious.nginx.enable then "127.0.0.1" else "0.0.0.0"'';
345 The IP address Invidious should bind to.
349 port = lib.mkOption {
351 # Default from https://docs.invidious.io/Configuration.md
354 The port Invidious should listen on.
356 To allow access from outside,
357 you can use either {option}`services.invidious.nginx`
358 or add `config.services.invidious.port` to {option}`networking.firewall.allowedTCPPorts`.
363 createLocally = lib.mkOption {
367 Whether to create a local database with PostgreSQL.
371 host = lib.mkOption {
372 type = types.nullOr types.str;
375 The database host Invidious should use.
377 If `null`, the local unix socket is used. Otherwise
382 port = lib.mkOption {
384 default = config.services.postgresql.settings.port;
385 defaultText = lib.literalExpression "config.services.postgresql.settings.port";
387 The port of the database Invidious should use.
389 Defaults to the the default postgresql port.
393 passwordFile = lib.mkOption {
394 type = types.nullOr types.str;
395 apply = lib.mapNullable toString;
398 Path to file containing the database password.
403 nginx.enable = lib.mkOption {
407 Whether to configure nginx as a reverse proxy for Invidious.
409 It serves it under the domain specified in {option}`services.invidious.settings.domain` with enabled TLS and ACME.
410 Further configuration can be done through {option}`services.nginx.virtualHosts.''${config.services.invidious.settings.domain}.*`,
411 which can also be used to disable AMCE and TLS.
416 enable = lib.mkOption {
417 type = lib.types.bool;
420 Whether to enable http3-ytproxy for faster loading of images and video playback.
422 If {option}`services.invidious.nginx.enable` is used, nginx will be configured automatically. If not, you
423 need to configure a reverse proxy yourself according to
424 https://docs.invidious.io/improve-public-instance/#3-speed-up-video-playback-with-http3-ytproxy.
428 package = lib.mkPackageOption pkgs "http3-ytproxy" { };
432 enable = lib.mkOption {
433 type = lib.types.bool;
436 Whether to enable and configure inv-sig-helper to emulate the youtube client's javascript. This is required
437 to make certain videos playable.
439 This will download and run completely untrusted javascript from youtube! While this service is sandboxed,
440 this may still be an issue!
444 package = lib.mkPackageOption pkgs "inv-sig-helper" { };
446 listenAddress = lib.mkOption {
447 type = lib.types.str;
448 default = "127.0.0.1:2999";
450 The IP address/port where inv-sig-helper should listen.
456 config = lib.mkIf cfg.enable (lib.mkMerge [