1{ lib, config, pkgs, options, ... }:
2let
3 cfg = config.services.invidious;
4 # To allow injecting secrets with jq, json (instead of yaml) is used
5 settingsFormat = pkgs.formats.json { };
6 inherit (lib) types;
7
8 settingsFile = settingsFormat.generate "invidious-settings" cfg.settings;
9
10 serviceConfig = {
11 systemd.services.invidious = {
12 description = "Invidious (An alternative YouTube front-end)";
13 wants = [ "network-online.target" ];
14 after = [ "network-online.target" ];
15 wantedBy = [ "multi-user.target" ];
16
17 script =
18 let
19 jqFilter = "."
20 + lib.optionalString (cfg.database.host != null) "[0].db.password = \"'\"'\"$(cat ${lib.escapeShellArg cfg.database.passwordFile})\"'\"'\""
21 + " | .[0]"
22 + lib.optionalString (cfg.extraSettingsFile != null) " * .[1]";
23 jqFiles = [ settingsFile ] ++ lib.optional (cfg.extraSettingsFile != null) cfg.extraSettingsFile;
24 in
25 ''
26 export INVIDIOUS_CONFIG="$(${pkgs.jq}/bin/jq -s "${jqFilter}" ${lib.escapeShellArgs jqFiles})"
27 exec ${cfg.package}/bin/invidious
28 '';
29
30 serviceConfig = {
31 RestartSec = "2s";
32 DynamicUser = true;
33
34 CapabilityBoundingSet = "";
35 PrivateDevices = true;
36 PrivateUsers = true;
37 ProtectHome = true;
38 ProtectKernelLogs = true;
39 ProtectProc = "invisible";
40 RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
41 RestrictNamespaces = true;
42 SystemCallArchitectures = "native";
43 SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
44 };
45 };
46
47 services.invidious.settings = {
48 inherit (cfg) port;
49
50 # Automatically initialises and migrates the database if necessary
51 check_tables = true;
52
53 db = {
54 user = lib.mkDefault "kemal";
55 dbname = lib.mkDefault "invidious";
56 port = cfg.database.port;
57 # Blank for unix sockets, see
58 # https://github.com/will/crystal-pg/blob/1548bb255210/src/pq/conninfo.cr#L100-L108
59 host = if cfg.database.host == null then "" else cfg.database.host;
60 # Not needed because peer authentication is enabled
61 password = lib.mkIf (cfg.database.host == null) "";
62 };
63 } // (lib.optionalAttrs (cfg.domain != null) {
64 inherit (cfg) domain;
65 });
66
67 assertions = [{
68 assertion = cfg.database.host != null -> cfg.database.passwordFile != null;
69 message = "If database host isn't null, database password needs to be set";
70 }];
71 };
72
73 # Settings necessary for running with an automatically managed local database
74 localDatabaseConfig = lib.mkIf cfg.database.createLocally {
75 # Default to using the local database if we create it
76 services.invidious.database.host = lib.mkDefault null;
77
78 services.postgresql = {
79 enable = true;
80 ensureDatabases = lib.singleton cfg.settings.db.dbname;
81 ensureUsers = lib.singleton {
82 name = cfg.settings.db.user;
83 ensurePermissions = {
84 "DATABASE ${cfg.settings.db.dbname}" = "ALL PRIVILEGES";
85 };
86 };
87 # This is only needed because the unix user invidious isn't the same as
88 # the database user. This tells postgres to map one to the other.
89 identMap = ''
90 invidious invidious ${cfg.settings.db.user}
91 '';
92 # And this specifically enables peer authentication for only this
93 # database, which allows passwordless authentication over the postgres
94 # unix socket for the user map given above.
95 authentication = ''
96 local ${cfg.settings.db.dbname} ${cfg.settings.db.user} peer map=invidious
97 '';
98 };
99
100 systemd.services.invidious-db-clean = {
101 description = "Invidious database cleanup";
102 documentation = [ "https://docs.invidious.io/Database-Information-and-Maintenance.md" ];
103 startAt = lib.mkDefault "weekly";
104 path = [ config.services.postgresql.package ];
105 script = ''
106 psql ${cfg.settings.db.dbname} ${cfg.settings.db.user} -c "DELETE FROM nonces * WHERE expire < current_timestamp"
107 psql ${cfg.settings.db.dbname} ${cfg.settings.db.user} -c "TRUNCATE TABLE videos"
108 '';
109 serviceConfig = {
110 DynamicUser = true;
111 User = "invidious";
112 };
113 };
114
115 systemd.services.invidious = {
116 requires = [ "postgresql.service" ];
117 after = [ "postgresql.service" ];
118
119 serviceConfig = {
120 User = "invidious";
121 };
122 };
123 };
124
125 nginxConfig = lib.mkIf cfg.nginx.enable {
126 services.invidious.settings = {
127 https_only = config.services.nginx.virtualHosts.${cfg.domain}.forceSSL;
128 external_port = 80;
129 };
130
131 services.nginx = {
132 enable = true;
133 virtualHosts.${cfg.domain} = {
134 locations."/".proxyPass = "http://127.0.0.1:${toString cfg.port}";
135
136 enableACME = lib.mkDefault true;
137 forceSSL = lib.mkDefault true;
138 };
139 };
140
141 assertions = [{
142 assertion = cfg.domain != null;
143 message = "To use services.invidious.nginx, you need to set services.invidious.domain";
144 }];
145 };
146in
147{
148 options.services.invidious = {
149 enable = lib.mkEnableOption (lib.mdDoc "Invidious");
150
151 package = lib.mkOption {
152 type = types.package;
153 default = pkgs.invidious;
154 defaultText = lib.literalExpression "pkgs.invidious";
155 description = lib.mdDoc "The Invidious package to use.";
156 };
157
158 settings = lib.mkOption {
159 type = settingsFormat.type;
160 default = { };
161 description = lib.mdDoc ''
162 The settings Invidious should use.
163
164 See [config.example.yml](https://github.com/iv-org/invidious/blob/master/config/config.example.yml) for a list of all possible options.
165 '';
166 };
167
168 extraSettingsFile = lib.mkOption {
169 type = types.nullOr types.str;
170 default = null;
171 description = lib.mdDoc ''
172 A file including Invidious settings.
173
174 It gets merged with the settings specified in {option}`services.invidious.settings`
175 and can be used to store secrets like `hmac_key` outside of the nix store.
176 '';
177 };
178
179 # This needs to be outside of settings to avoid infinite recursion
180 # (determining if nginx should be enabled and therefore the settings
181 # modified).
182 domain = lib.mkOption {
183 type = types.nullOr types.str;
184 default = null;
185 description = lib.mdDoc ''
186 The FQDN Invidious is reachable on.
187
188 This is used to configure nginx and for building absolute URLs.
189 '';
190 };
191
192 port = lib.mkOption {
193 type = types.port;
194 # Default from https://docs.invidious.io/Configuration.md
195 default = 3000;
196 description = lib.mdDoc ''
197 The port Invidious should listen on.
198
199 To allow access from outside,
200 you can use either {option}`services.invidious.nginx`
201 or add `config.services.invidious.port` to {option}`networking.firewall.allowedTCPPorts`.
202 '';
203 };
204
205 database = {
206 createLocally = lib.mkOption {
207 type = types.bool;
208 default = true;
209 description = lib.mdDoc ''
210 Whether to create a local database with PostgreSQL.
211 '';
212 };
213
214 host = lib.mkOption {
215 type = types.nullOr types.str;
216 default = null;
217 description = lib.mdDoc ''
218 The database host Invidious should use.
219
220 If `null`, the local unix socket is used. Otherwise
221 TCP is used.
222 '';
223 };
224
225 port = lib.mkOption {
226 type = types.port;
227 default = options.services.postgresql.port.default;
228 defaultText = lib.literalExpression "options.services.postgresql.port.default";
229 description = lib.mdDoc ''
230 The port of the database Invidious should use.
231
232 Defaults to the the default postgresql port.
233 '';
234 };
235
236 passwordFile = lib.mkOption {
237 type = types.nullOr types.str;
238 apply = lib.mapNullable toString;
239 default = null;
240 description = lib.mdDoc ''
241 Path to file containing the database password.
242 '';
243 };
244 };
245
246 nginx.enable = lib.mkOption {
247 type = types.bool;
248 default = false;
249 description = lib.mdDoc ''
250 Whether to configure nginx as a reverse proxy for Invidious.
251
252 It serves it under the domain specified in {option}`services.invidious.settings.domain` with enabled TLS and ACME.
253 Further configuration can be done through {option}`services.nginx.virtualHosts.''${config.services.invidious.settings.domain}.*`,
254 which can also be used to disable AMCE and TLS.
255 '';
256 };
257 };
258
259 config = lib.mkIf cfg.enable (lib.mkMerge [
260 serviceConfig
261 localDatabaseConfig
262 nginxConfig
263 ]);
264}