mirror of
https://github.com/NixOS/nixpkgs.git
synced 2025-06-27 11:36:29 +03:00
Merge pull request #265857 from 999eagle/feat/invidious-scale
nixos/invidious: cleanup, allow for easy scaling and load balancing, add http3-ytproxy
This commit is contained in:
commit
17c3ebdba5
5 changed files with 702 additions and 148 deletions
|
@ -10,82 +10,115 @@ let
|
|||
generatedHmacKeyFile = "/var/lib/invidious/hmac_key";
|
||||
generateHmac = cfg.hmacKeyFile == null;
|
||||
|
||||
serviceConfig = {
|
||||
systemd.services.invidious = {
|
||||
description = "Invidious (An alternative YouTube front-end)";
|
||||
wants = [ "network-online.target" ];
|
||||
after = [ "network-online.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
commonInvidousServiceConfig = {
|
||||
description = "Invidious (An alternative YouTube front-end)";
|
||||
wants = [ "network-online.target" ];
|
||||
after = [ "network-online.target" ] ++ lib.optional cfg.database.createLocally "postgresql.service";
|
||||
requires = lib.optional cfg.database.createLocally "postgresql.service";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
preStart = lib.optionalString generateHmac ''
|
||||
if [[ ! -e "${generatedHmacKeyFile}" ]]; then
|
||||
${pkgs.pwgen}/bin/pwgen 20 1 > "${generatedHmacKeyFile}"
|
||||
chmod 0600 "${generatedHmacKeyFile}"
|
||||
fi
|
||||
'';
|
||||
serviceConfig = {
|
||||
RestartSec = "2s";
|
||||
DynamicUser = true;
|
||||
User = lib.mkIf (cfg.database.createLocally || cfg.serviceScale > 1) "invidious";
|
||||
StateDirectory = "invidious";
|
||||
StateDirectoryMode = "0750";
|
||||
|
||||
script = ''
|
||||
configParts=()
|
||||
''
|
||||
# autogenerated hmac_key
|
||||
+ lib.optionalString generateHmac ''
|
||||
configParts+=("$(${pkgs.jq}/bin/jq -R '{"hmac_key":.}' <"${generatedHmacKeyFile}")")
|
||||
''
|
||||
# generated settings file
|
||||
+ ''
|
||||
configParts+=("$(< ${lib.escapeShellArg settingsFile})")
|
||||
''
|
||||
# optional database password file
|
||||
+ lib.optionalString (cfg.database.host != null) ''
|
||||
configParts+=("$(${pkgs.jq}/bin/jq -R '{"db":{"password":.}}' ${lib.escapeShellArg cfg.database.passwordFile})")
|
||||
''
|
||||
# optional extra settings file
|
||||
+ lib.optionalString (cfg.extraSettingsFile != null) ''
|
||||
configParts+=("$(< ${lib.escapeShellArg cfg.extraSettingsFile})")
|
||||
''
|
||||
# explicitly specified hmac key file
|
||||
+ lib.optionalString (cfg.hmacKeyFile != null) ''
|
||||
configParts+=("$(< ${lib.escapeShellArg cfg.hmacKeyFile})")
|
||||
''
|
||||
# merge all parts into a single configuration with later elements overriding previous elements
|
||||
+ ''
|
||||
export INVIDIOUS_CONFIG="$(${pkgs.jq}/bin/jq -s 'reduce .[] as $item ({}; . * $item)' <<<"''${configParts[*]}")"
|
||||
exec ${cfg.package}/bin/invidious
|
||||
'';
|
||||
CapabilityBoundingSet = "";
|
||||
PrivateDevices = true;
|
||||
PrivateUsers = true;
|
||||
ProtectHome = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectProc = "invisible";
|
||||
RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
|
||||
RestrictNamespaces = true;
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
|
||||
|
||||
serviceConfig = {
|
||||
RestartSec = "2s";
|
||||
DynamicUser = true;
|
||||
StateDirectory = "invidious";
|
||||
StateDirectoryMode = "0750";
|
||||
|
||||
CapabilityBoundingSet = "";
|
||||
PrivateDevices = true;
|
||||
PrivateUsers = true;
|
||||
ProtectHome = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectProc = "invisible";
|
||||
RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
|
||||
RestrictNamespaces = true;
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
|
||||
|
||||
# Because of various issues Invidious must be restarted often, at least once a day, ideally
|
||||
# every hour.
|
||||
# This option enables the automatic restarting of the Invidious instance.
|
||||
Restart = lib.mkDefault "always";
|
||||
RuntimeMaxSec = lib.mkDefault "1h";
|
||||
};
|
||||
# Because of various issues Invidious must be restarted often, at least once a day, ideally
|
||||
# every hour.
|
||||
# This option enables the automatic restarting of the Invidious instance.
|
||||
# To ensure multiple instances of Invidious are not restarted at the exact same time, a
|
||||
# randomized extra offset of up to 5 minutes is added.
|
||||
Restart = lib.mkDefault "always";
|
||||
RuntimeMaxSec = lib.mkDefault "1h";
|
||||
RuntimeRandomizedExtraSec = lib.mkDefault "5min";
|
||||
};
|
||||
};
|
||||
mkInvidiousService = scaleIndex:
|
||||
lib.foldl' lib.recursiveUpdate commonInvidousServiceConfig [
|
||||
# only generate the hmac file in the first service
|
||||
(lib.optionalAttrs (scaleIndex == 0) {
|
||||
preStart = lib.optionalString generateHmac ''
|
||||
if [[ ! -e "${generatedHmacKeyFile}" ]]; then
|
||||
${pkgs.pwgen}/bin/pwgen 20 1 > "${generatedHmacKeyFile}"
|
||||
chmod 0600 "${generatedHmacKeyFile}"
|
||||
fi
|
||||
'';
|
||||
})
|
||||
# configure the secondary services to run after the first service
|
||||
(lib.optionalAttrs (scaleIndex > 0) {
|
||||
after = commonInvidousServiceConfig.after ++ [ "invidious.service" ];
|
||||
wants = commonInvidousServiceConfig.wants ++ [ "invidious.service" ];
|
||||
})
|
||||
{
|
||||
script = ''
|
||||
configParts=()
|
||||
''
|
||||
# autogenerated hmac_key
|
||||
+ lib.optionalString generateHmac ''
|
||||
configParts+=("$(${pkgs.jq}/bin/jq -R '{"hmac_key":.}' <"${generatedHmacKeyFile}")")
|
||||
''
|
||||
# generated settings file
|
||||
+ ''
|
||||
configParts+=("$(< ${lib.escapeShellArg settingsFile})")
|
||||
''
|
||||
# optional database password file
|
||||
+ lib.optionalString (cfg.database.host != null) ''
|
||||
configParts+=("$(${pkgs.jq}/bin/jq -R '{"db":{"password":.}}' ${lib.escapeShellArg cfg.database.passwordFile})")
|
||||
''
|
||||
# optional extra settings file
|
||||
+ lib.optionalString (cfg.extraSettingsFile != null) ''
|
||||
configParts+=("$(< ${lib.escapeShellArg cfg.extraSettingsFile})")
|
||||
''
|
||||
# explicitly specified hmac key file
|
||||
+ lib.optionalString (cfg.hmacKeyFile != null) ''
|
||||
configParts+=("$(< ${lib.escapeShellArg cfg.hmacKeyFile})")
|
||||
''
|
||||
# configure threads for secondary instances
|
||||
+ lib.optionalString (scaleIndex > 0) ''
|
||||
configParts+=('{"channel_threads":0, "feed_threads":0}')
|
||||
''
|
||||
# configure different ports for the instances
|
||||
+ ''
|
||||
configParts+=('{"port":${toString (cfg.port + scaleIndex)}}')
|
||||
''
|
||||
# merge all parts into a single configuration with later elements overriding previous elements
|
||||
+ ''
|
||||
export INVIDIOUS_CONFIG="$(${pkgs.jq}/bin/jq -s 'reduce .[] as $item ({}; . * $item)' <<<"''${configParts[*]}")"
|
||||
exec ${cfg.package}/bin/invidious
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
serviceConfig = {
|
||||
systemd.services = builtins.listToAttrs (builtins.genList
|
||||
(scaleIndex: {
|
||||
name = "invidious" + lib.optionalString (scaleIndex > 0) "-${builtins.toString scaleIndex}";
|
||||
value = mkInvidiousService scaleIndex;
|
||||
})
|
||||
cfg.serviceScale);
|
||||
|
||||
services.invidious.settings = {
|
||||
inherit (cfg) port;
|
||||
|
||||
# Automatically initialises and migrates the database if necessary
|
||||
check_tables = true;
|
||||
|
||||
db = {
|
||||
user = lib.mkDefault "kemal";
|
||||
user = lib.mkDefault (
|
||||
if (lib.versionAtLeast config.system.stateVersion "24.05")
|
||||
then "invidious"
|
||||
else "kemal"
|
||||
);
|
||||
dbname = lib.mkDefault "invidious";
|
||||
port = cfg.database.port;
|
||||
# Blank for unix sockets, see
|
||||
|
@ -94,67 +127,73 @@ let
|
|||
# Not needed because peer authentication is enabled
|
||||
password = lib.mkIf (cfg.database.host == null) "";
|
||||
};
|
||||
|
||||
host_binding = cfg.address;
|
||||
} // (lib.optionalAttrs (cfg.domain != null) {
|
||||
inherit (cfg) domain;
|
||||
});
|
||||
|
||||
assertions = [{
|
||||
assertion = cfg.database.host != null -> cfg.database.passwordFile != null;
|
||||
message = "If database host isn't null, database password needs to be set";
|
||||
}];
|
||||
assertions = [
|
||||
{
|
||||
assertion = cfg.database.host != null -> cfg.database.passwordFile != null;
|
||||
message = "If database host isn't null, database password needs to be set";
|
||||
}
|
||||
{
|
||||
assertion = cfg.serviceScale >= 1;
|
||||
message = "Service can't be scaled below one instance";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
# Settings necessary for running with an automatically managed local database
|
||||
localDatabaseConfig = lib.mkIf cfg.database.createLocally {
|
||||
assertions = [
|
||||
{
|
||||
assertion = cfg.settings.db.user == cfg.settings.db.dbname;
|
||||
message = ''
|
||||
For local automatic database provisioning (services.invidious.database.createLocally == true)
|
||||
to work, the username used to connect to PostgreSQL must match the database name, that is
|
||||
services.invidious.database.user must match services.invidious.database.dbName.
|
||||
This is the default since NixOS 24.05. For older systems, it is normally safe to manually set
|
||||
services.invidious.database.user to "invidious" as the new user will be created with permissions
|
||||
for the existing database.
|
||||
'';
|
||||
}
|
||||
];
|
||||
# Default to using the local database if we create it
|
||||
services.invidious.database.host = lib.mkDefault null;
|
||||
|
||||
|
||||
# TODO(raitobezarius to maintainers of invidious): I strongly advise to clean up the kemal specific
|
||||
# thing for 24.05 and use `ensureDBOwnership`.
|
||||
# See https://github.com/NixOS/nixpkgs/issues/216989
|
||||
systemd.services.postgresql.postStart = lib.mkAfter ''
|
||||
$PSQL -tAc 'ALTER DATABASE "${cfg.settings.db.dbname}" OWNER TO "${cfg.settings.db.user}";'
|
||||
'';
|
||||
services.postgresql = {
|
||||
enable = true;
|
||||
ensureUsers = lib.singleton { name = cfg.settings.db.user; ensureDBOwnership = false; };
|
||||
ensureUsers = lib.singleton { name = cfg.settings.db.user; ensureDBOwnership = true; };
|
||||
ensureDatabases = lib.singleton cfg.settings.db.dbname;
|
||||
# This is only needed because the unix user invidious isn't the same as
|
||||
# the database user. This tells postgres to map one to the other.
|
||||
identMap = ''
|
||||
invidious invidious ${cfg.settings.db.user}
|
||||
'';
|
||||
# And this specifically enables peer authentication for only this
|
||||
# database, which allows passwordless authentication over the postgres
|
||||
# unix socket for the user map given above.
|
||||
authentication = ''
|
||||
local ${cfg.settings.db.dbname} ${cfg.settings.db.user} peer map=invidious
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
ytproxyConfig = lib.mkIf cfg.http3-ytproxy.enable {
|
||||
systemd.services.http3-ytproxy = {
|
||||
description = "HTTP3 ytproxy for Invidious";
|
||||
wants = [ "network-online.target" ];
|
||||
after = [ "network-online.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
systemd.services.invidious-db-clean = {
|
||||
description = "Invidious database cleanup";
|
||||
documentation = [ "https://docs.invidious.io/Database-Information-and-Maintenance.md" ];
|
||||
startAt = lib.mkDefault "weekly";
|
||||
path = [ config.services.postgresql.package ];
|
||||
after = [ "postgresql.service" ];
|
||||
script = ''
|
||||
psql ${cfg.settings.db.dbname} ${cfg.settings.db.user} -c "DELETE FROM nonces * WHERE expire < current_timestamp"
|
||||
psql ${cfg.settings.db.dbname} ${cfg.settings.db.user} -c "TRUNCATE TABLE videos"
|
||||
mkdir -p socket
|
||||
exec ${lib.getExe cfg.http3-ytproxy.package};
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
RestartSec = "2s";
|
||||
DynamicUser = true;
|
||||
User = "invidious";
|
||||
User = lib.mkIf cfg.nginx.enable config.services.nginx.user;
|
||||
RuntimeDirectory = "http3-ytproxy";
|
||||
WorkingDirectory = "/run/http3-ytproxy";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.invidious = {
|
||||
requires = [ "postgresql.service" ];
|
||||
after = [ "postgresql.service" ];
|
||||
|
||||
serviceConfig = {
|
||||
User = "invidious";
|
||||
services.nginx.virtualHosts.${cfg.domain} = lib.mkIf cfg.nginx.enable {
|
||||
locations."~ (^/videoplayback|^/vi/|^/ggpht/|^/sb/)" = {
|
||||
proxyPass = "http://unix:/run/http3-ytproxy/socket/http-proxy.sock";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -165,14 +204,28 @@ let
|
|||
external_port = 80;
|
||||
};
|
||||
|
||||
services.nginx = {
|
||||
services.nginx = let
|
||||
ip = if cfg.address == "0.0.0.0" then "127.0.0.1" else cfg.address;
|
||||
in
|
||||
{
|
||||
enable = true;
|
||||
virtualHosts.${cfg.domain} = {
|
||||
locations."/".proxyPass = "http://127.0.0.1:${toString cfg.port}";
|
||||
locations."/".proxyPass =
|
||||
if cfg.serviceScale == 1 then
|
||||
"http://${ip}:${toString cfg.port}"
|
||||
else "http://upstream-invidious";
|
||||
|
||||
enableACME = lib.mkDefault true;
|
||||
forceSSL = lib.mkDefault true;
|
||||
};
|
||||
upstreams = lib.mkIf (cfg.serviceScale > 1) {
|
||||
"upstream-invidious".servers = builtins.listToAttrs (builtins.genList
|
||||
(scaleIndex: {
|
||||
name = "${ip}:${toString (cfg.port + scaleIndex)}";
|
||||
value = { };
|
||||
})
|
||||
cfg.serviceScale);
|
||||
};
|
||||
};
|
||||
|
||||
assertions = [{
|
||||
|
@ -220,6 +273,20 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
serviceScale = lib.mkOption {
|
||||
type = types.int;
|
||||
default = 1;
|
||||
description = lib.mdDoc ''
|
||||
How many invidious instances to run.
|
||||
|
||||
See https://docs.invidious.io/improve-public-instance/#2-multiple-invidious-processes for more details
|
||||
on how this is intended to work. All instances beyond the first one have the options `channel_threads`
|
||||
and `feed_threads` set to 0 to avoid conflicts with multiple instances refreshing subscriptions. Instances
|
||||
will be configured to bind to consecutive ports starting with {option}`services.invidious.port` for the
|
||||
first instance.
|
||||
'';
|
||||
};
|
||||
|
||||
# This needs to be outside of settings to avoid infinite recursion
|
||||
# (determining if nginx should be enabled and therefore the settings
|
||||
# modified).
|
||||
|
@ -233,6 +300,16 @@ in
|
|||
'';
|
||||
};
|
||||
|
||||
address = lib.mkOption {
|
||||
type = types.str;
|
||||
# default from https://github.com/iv-org/invidious/blob/master/config/config.example.yml
|
||||
default = if cfg.nginx.enable then "127.0.0.1" else "0.0.0.0";
|
||||
defaultText = lib.literalExpression ''if config.services.invidious.nginx.enable then "127.0.0.1" else "0.0.0.0"'';
|
||||
description = lib.mdDoc ''
|
||||
The IP address Invidious should bind to.
|
||||
'';
|
||||
};
|
||||
|
||||
port = lib.mkOption {
|
||||
type = types.port;
|
||||
# Default from https://docs.invidious.io/Configuration.md
|
||||
|
@ -298,11 +375,28 @@ in
|
|||
which can also be used to disable AMCE and TLS.
|
||||
'';
|
||||
};
|
||||
|
||||
http3-ytproxy = {
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = lib.mdDoc ''
|
||||
Whether to enable http3-ytproxy for faster loading of images and video playback.
|
||||
|
||||
If {option}`services.invidious.nginx.enable` is used, nginx will be configured automatically. If not, you
|
||||
need to configure a reverse proxy yourself according to
|
||||
https://docs.invidious.io/improve-public-instance/#3-speed-up-video-playback-with-http3-ytproxy.
|
||||
'';
|
||||
};
|
||||
|
||||
package = lib.mkPackageOptionMD pkgs "http3-ytproxy" { };
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable (lib.mkMerge [
|
||||
serviceConfig
|
||||
localDatabaseConfig
|
||||
nginxConfig
|
||||
ytproxyConfig
|
||||
]);
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue