···45This section uses [Mint](https://github.com/mint-lang/mint) as an example for how to build a Crystal package.
67-If the Crystal project has any dependencies, the first step is to get a `shards.nix` file encoding those. Get a copy of the project and go to its root directory such that its `shard.lock` file is in the current directory, then run `crystal2nix` in it
8-9```bash
10$ git clone https://github.com/mint-lang/mint
11$ cd mint
12$ git checkout 0.5.0
013$ nix-shell -p crystal2nix --run crystal2nix
14```
15
···45This section uses [Mint](https://github.com/mint-lang/mint) as an example for how to build a Crystal package.
67+If the Crystal project has any dependencies, the first step is to get a `shards.nix` file encoding those. Get a copy of the project and go to its root directory such that its `shard.lock` file is in the current directory. Executable projects should usually commit the `shard.lock` file, but sometimes that's not the case, which means you need to generate it yourself. With an existing `shard.lock` file, `crystal2nix` can be run.
08```bash
9$ git clone https://github.com/mint-lang/mint
10$ cd mint
11$ git checkout 0.5.0
12+$ if [ ! -f shard.lock ]; then nix-shell -p shards --run "shards lock"; fi
13$ nix-shell -p crystal2nix --run crystal2nix
14```
15
···1+{ lib, config, pkgs, options, ... }:
2+let
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 = [ "syslog.target" "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+ };
146+in
147+{
148+ options.services.invidious = {
149+ enable = lib.mkEnableOption "Invidious";
150+151+ package = lib.mkOption {
152+ type = types.package;
153+ default = pkgs.invidious;
154+ defaultText = "pkgs.invidious";
155+ description = "The Invidious package to use.";
156+ };
157+158+ settings = lib.mkOption {
159+ type = settingsFormat.type;
160+ default = { };
161+ description = ''
162+ The settings Invidious should use.
163+164+ See <link xlink:href="https://github.com/iv-org/invidious/blob/master/config/config.example.yml">config.example.yml</link> 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 = ''
172+ A file including Invidious settings.
173+174+ It gets merged with the setttings specified in <option>services.invidious.settings</option>
175+ and can be used to store secrets like <literal>hmac_key</literal> 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 = ''
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 = ''
197+ The port Invidious should listen on.
198+199+ To allow access from outside,
200+ you can use either <option>services.invidious.nginx</option>
201+ or add <literal>config.services.invidious.port</literal> to <option>networking.firewall.allowedTCPPorts</option>.
202+ '';
203+ };
204+205+ database = {
206+ createLocally = lib.mkOption {
207+ type = types.bool;
208+ default = true;
209+ description = ''
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 = ''
218+ The database host Invidious should use.
219+220+ If <literal>null</literal>, 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+ description = ''
229+ The port of the database Invidious should use.
230+231+ Defaults to the the default postgresql port.
232+ '';
233+ };
234+235+ passwordFile = lib.mkOption {
236+ type = types.nullOr types.str;
237+ apply = lib.mapNullable toString;
238+ default = null;
239+ description = ''
240+ Path to file containing the database password.
241+ '';
242+ };
243+ };
244+245+ nginx.enable = lib.mkOption {
246+ type = types.bool;
247+ default = false;
248+ description = ''
249+ Whether to configure nginx as a reverse proxy for Invidious.
250+251+ It serves it under the domain specified in <option>services.invidious.settings.domain</option> with enabled TLS and ACME.
252+ Further configuration can be done through <option>services.nginx.virtualHosts.''${config.services.invidious.settings.domain}.*</option>,
253+ which can also be used to disable AMCE and TLS.
254+ '';
255+ };
256+ };
257+258+ config = lib.mkIf cfg.enable (lib.mkMerge [
259+ serviceConfig
260+ localDatabaseConfig
261+ nginxConfig
262+ ]);
263+}
+1
nixos/tests/all-tests.nix
···173 hedgedoc = handleTest ./hedgedoc.nix {};
174 herbstluftwm = handleTest ./herbstluftwm.nix {};
175 installed-tests = pkgs.recurseIntoAttrs (handleTest ./installed-tests {});
0176 oci-containers = handleTestOn ["x86_64-linux"] ./oci-containers.nix {};
177 # 9pnet_virtio used to mount /nix partition doesn't support
178 # hibernation. This test happens to work on x86_64-linux but
···173 hedgedoc = handleTest ./hedgedoc.nix {};
174 herbstluftwm = handleTest ./herbstluftwm.nix {};
175 installed-tests = pkgs.recurseIntoAttrs (handleTest ./installed-tests {});
176+ invidious = handleTest ./invidious.nix {};
177 oci-containers = handleTestOn ["x86_64-linux"] ./oci-containers.nix {};
178 # 9pnet_virtio used to mount /nix partition doesn't support
179 # hibernation. This test happens to work on x86_64-linux but