about summary refs log tree commit diff
path: root/nixpkgs/nixos/modules/services/web-apps/invidious.nix
blob: 8823da0100141e2980b9e92aa4b96a912a24bb92 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
{ 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 = [ "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" ];

        # 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";
      };
    };

    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 = lib.optionalString (cfg.database.host != null) 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 (lib.mdDoc "Invidious");

    package = lib.mkOption {
      type = types.package;
      default = pkgs.invidious;
      defaultText = lib.literalExpression "pkgs.invidious";
      description = lib.mdDoc "The Invidious package to use.";
    };

    settings = lib.mkOption {
      type = settingsFormat.type;
      default = { };
      description = lib.mdDoc ''
        The settings Invidious should use.

        See [config.example.yml](https://github.com/iv-org/invidious/blob/master/config/config.example.yml) for a list of all possible options.
      '';
    };

    extraSettingsFile = lib.mkOption {
      type = types.nullOr types.str;
      default = null;
      description = lib.mdDoc ''
        A file including Invidious settings.

        It gets merged with the settings specified in {option}`services.invidious.settings`
        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 = lib.mdDoc ''
        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 = lib.mdDoc ''
        The port Invidious should listen on.

        To allow access from outside,
        you can use either {option}`services.invidious.nginx`
        or add `config.services.invidious.port` to {option}`networking.firewall.allowedTCPPorts`.
      '';
    };

    database = {
      createLocally = lib.mkOption {
        type = types.bool;
        default = true;
        description = lib.mdDoc ''
          Whether to create a local database with PostgreSQL.
        '';
      };

      host = lib.mkOption {
        type = types.nullOr types.str;
        default = null;
        description = lib.mdDoc ''
          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;
        defaultText = lib.literalExpression "options.services.postgresql.port.default";
        description = lib.mdDoc ''
          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 = lib.mdDoc ''
          Path to file containing the database password.
        '';
      };
    };

    nginx.enable = lib.mkOption {
      type = types.bool;
      default = false;
      description = lib.mdDoc ''
        Whether to configure nginx as a reverse proxy for Invidious.

        It serves it under the domain specified in {option}`services.invidious.settings.domain` with enabled TLS and ACME.
        Further configuration can be done through {option}`services.nginx.virtualHosts.''${config.services.invidious.settings.domain}.*`,
        which can also be used to disable AMCE and TLS.
      '';
    };
  };

  config = lib.mkIf cfg.enable (lib.mkMerge [
    serviceConfig
    localDatabaseConfig
    nginxConfig
  ]);
}