diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 5610813d9ad0..00d0103e6b79 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -992,6 +992,7 @@
./services/web-apps/jitsi-meet.nix
./services/web-apps/keycloak.nix
./services/web-apps/lemmy.nix
+ ./services/web-apps/invidious.nix
./services/web-apps/limesurvey.nix
./services/web-apps/mastodon.nix
./services/web-apps/mattermost.nix
diff --git a/nixos/modules/services/web-apps/invidious.nix b/nixos/modules/services/web-apps/invidious.nix
new file mode 100644
index 000000000000..7fb826af5835
--- /dev/null
+++ b/nixos/modules/services/web-apps/invidious.nix
@@ -0,0 +1,263 @@
+{ lib, config, pkgs, options, ... }:
+let
+ cfg = config.services.invidious;
+ # To allow injecting secrets with jq, json (instead of yaml) is used
+ settingsFormat = pkgs.formats.json { };
+ inherit (lib) types;
+
+ settingsFile = settingsFormat.generate "invidious-settings" cfg.settings;
+
+ serviceConfig = {
+ systemd.services.invidious = {
+ description = "Invidious (An alternative YouTube front-end)";
+ wants = [ "network-online.target" ];
+ after = [ "syslog.target" "network-online.target" ];
+ wantedBy = [ "multi-user.target" ];
+
+ script =
+ let
+ jqFilter = "."
+ + lib.optionalString (cfg.database.host != null) "[0].db.password = \"'\"'\"$(cat ${lib.escapeShellArg cfg.database.passwordFile})\"'\"'\""
+ + " | .[0]"
+ + lib.optionalString (cfg.extraSettingsFile != null) " * .[1]";
+ jqFiles = [ settingsFile ] ++ lib.optional (cfg.extraSettingsFile != null) cfg.extraSettingsFile;
+ in
+ ''
+ export INVIDIOUS_CONFIG="$(${pkgs.jq}/bin/jq -s "${jqFilter}" ${lib.escapeShellArgs jqFiles})"
+ exec ${cfg.package}/bin/invidious
+ '';
+
+ serviceConfig = {
+ RestartSec = "2s";
+ DynamicUser = true;
+
+ 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" ];
+ };
+ };
+
+ services.invidious.settings = {
+ inherit (cfg) port;
+
+ # Automatically initialises and migrates the database if necessary
+ check_tables = true;
+
+ db = {
+ user = lib.mkDefault "kemal";
+ dbname = lib.mkDefault "invidious";
+ port = cfg.database.port;
+ # Blank for unix sockets, see
+ # https://github.com/will/crystal-pg/blob/1548bb255210/src/pq/conninfo.cr#L100-L108
+ host = if cfg.database.host == null then "" else cfg.database.host;
+ # Not needed because peer authentication is enabled
+ password = lib.mkIf (cfg.database.host == null) "";
+ };
+ } // (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";
+ }];
+ };
+
+ # Settings necessary for running with an automatically managed local database
+ localDatabaseConfig = lib.mkIf cfg.database.createLocally {
+ # Default to using the local database if we create it
+ services.invidious.database.host = lib.mkDefault null;
+
+ services.postgresql = {
+ enable = true;
+ ensureDatabases = lib.singleton cfg.settings.db.dbname;
+ ensureUsers = lib.singleton {
+ name = cfg.settings.db.user;
+ ensurePermissions = {
+ "DATABASE ${cfg.settings.db.dbname}" = "ALL PRIVILEGES";
+ };
+ };
+ # 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
+ '';
+ };
+
+ 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 ];
+ 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"
+ '';
+ serviceConfig = {
+ DynamicUser = true;
+ User = "invidious";
+ };
+ };
+
+ systemd.services.invidious = {
+ requires = [ "postgresql.service" ];
+ after = [ "postgresql.service" ];
+
+ serviceConfig = {
+ User = "invidious";
+ };
+ };
+ };
+
+ nginxConfig = lib.mkIf cfg.nginx.enable {
+ services.invidious.settings = {
+ https_only = config.services.nginx.virtualHosts.${cfg.domain}.forceSSL;
+ external_port = 80;
+ };
+
+ services.nginx = {
+ enable = true;
+ virtualHosts.${cfg.domain} = {
+ locations."/".proxyPass = "http://127.0.0.1:${toString cfg.port}";
+
+ enableACME = lib.mkDefault true;
+ forceSSL = lib.mkDefault true;
+ };
+ };
+
+ assertions = [{
+ assertion = cfg.domain != null;
+ message = "To use services.invidious.nginx, you need to set services.invidious.domain";
+ }];
+ };
+in
+{
+ options.services.invidious = {
+ enable = lib.mkEnableOption "Invidious";
+
+ package = lib.mkOption {
+ type = types.package;
+ default = pkgs.invidious;
+ defaultText = "pkgs.invidious";
+ description = "The Invidious package to use.";
+ };
+
+ settings = lib.mkOption {
+ type = settingsFormat.type;
+ default = { };
+ description = ''
+ The settings Invidious should use.
+
+ See config.example.yml for a list of all possible options.
+ '';
+ };
+
+ extraSettingsFile = lib.mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ A file including Invidious settings.
+
+ It gets merged with the setttings specified in
+ and can be used to store secrets like hmac_key outside of the nix store.
+ '';
+ };
+
+ # This needs to be outside of settings to avoid infinite recursion
+ # (determining if nginx should be enabled and therefore the settings
+ # modified).
+ domain = lib.mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ The FQDN Invidious is reachable on.
+
+ This is used to configure nginx and for building absolute URLs.
+ '';
+ };
+
+ port = lib.mkOption {
+ type = types.port;
+ # Default from https://docs.invidious.io/Configuration.md
+ default = 3000;
+ description = ''
+ The port Invidious should listen on.
+
+ To allow access from outside,
+ you can use either
+ or add config.services.invidious.port to .
+ '';
+ };
+
+ database = {
+ createLocally = lib.mkOption {
+ type = types.bool;
+ default = true;
+ description = ''
+ Whether to create a local database with PostgreSQL.
+ '';
+ };
+
+ host = lib.mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ The database host Invidious should use.
+
+ If null, the local unix socket is used. Otherwise
+ TCP is used.
+ '';
+ };
+
+ port = lib.mkOption {
+ type = types.port;
+ default = options.services.postgresql.port.default;
+ description = ''
+ The port of the database Invidious should use.
+
+ Defaults to the the default postgresql port.
+ '';
+ };
+
+ passwordFile = lib.mkOption {
+ type = types.nullOr types.str;
+ apply = lib.mapNullable toString;
+ default = null;
+ description = ''
+ Path to file containing the database password.
+ '';
+ };
+ };
+
+ nginx.enable = lib.mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Whether to configure nginx as a reverse proxy for Invidious.
+
+ It serves it under the domain specified in with enabled TLS and ACME.
+ Further configuration can be done through ,
+ which can also be used to disable AMCE and TLS.
+ '';
+ };
+ };
+
+ config = lib.mkIf cfg.enable (lib.mkMerge [
+ serviceConfig
+ localDatabaseConfig
+ nginxConfig
+ ]);
+}