grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / web-apps / misskey.nix
blob8a5c4bd927660af34ba9ac0c4fcce892cf898357
2   config,
3   pkgs,
4   lib,
5   ...
6 }:
8 let
9   cfg = config.services.misskey;
10   settingsFormat = pkgs.formats.yaml { };
11   redisType = lib.types.submodule {
12     freeformType = lib.types.attrsOf settingsFormat.type;
13     options = {
14       host = lib.mkOption {
15         type = lib.types.str;
16         default = "localhost";
17         description = "The Redis host.";
18       };
19       port = lib.mkOption {
20         type = lib.types.port;
21         default = 6379;
22         description = "The Redis port.";
23       };
24     };
25   };
26   settings = lib.mkOption {
27     description = ''
28       Configuration for Misskey, see
29       [`example.yml`](https://github.com/misskey-dev/misskey/blob/develop/.config/example.yml)
30       for all supported options.
31     '';
32     type = lib.types.submodule {
33       freeformType = lib.types.attrsOf settingsFormat.type;
34       options = {
35         url = lib.mkOption {
36           type = lib.types.str;
37           example = "https://example.tld/";
38           description = ''
39             The final user-facing URL. Do not change after running Misskey for the first time.
41             This needs to match up with the configured reverse proxy and is automatically configured when using `services.misskey.reverseProxy`.
42           '';
43         };
44         port = lib.mkOption {
45           type = lib.types.port;
46           default = 3000;
47           description = "The port your Misskey server should listen on.";
48         };
49         socket = lib.mkOption {
50           type = lib.types.nullOr lib.types.path;
51           default = null;
52           example = "/path/to/misskey.sock";
53           description = "The UNIX socket your Misskey server should listen on.";
54         };
55         chmodSocket = lib.mkOption {
56           type = lib.types.nullOr lib.types.str;
57           default = null;
58           example = "777";
59           description = "The file access mode of the UNIX socket.";
60         };
61         db = lib.mkOption {
62           description = "Database settings.";
63           type = lib.types.submodule {
64             options = {
65               host = lib.mkOption {
66                 type = lib.types.str;
67                 default = "/var/run/postgresql";
68                 example = "localhost";
69                 description = "The PostgreSQL host.";
70               };
71               port = lib.mkOption {
72                 type = lib.types.port;
73                 default = 5432;
74                 description = "The PostgreSQL port.";
75               };
76               db = lib.mkOption {
77                 type = lib.types.str;
78                 default = "misskey";
79                 description = "The database name.";
80               };
81               user = lib.mkOption {
82                 type = lib.types.str;
83                 default = "misskey";
84                 description = "The user used for database authentication.";
85               };
86               pass = lib.mkOption {
87                 type = lib.types.nullOr lib.types.str;
88                 default = null;
89                 description = "The password used for database authentication.";
90               };
91               disableCache = lib.mkOption {
92                 type = lib.types.bool;
93                 default = false;
94                 description = "Whether to disable caching queries.";
95               };
96               extra = lib.mkOption {
97                 type = lib.types.nullOr (lib.types.attrsOf settingsFormat.type);
98                 default = null;
99                 example = {
100                   ssl = true;
101                 };
102                 description = "Extra connection options.";
103               };
104             };
105           };
106           default = { };
107         };
108         redis = lib.mkOption {
109           type = redisType;
110           default = { };
111           description = "`ioredis` options. See [`README`](https://github.com/redis/ioredis?tab=readme-ov-file#connect-to-redis) for reference.";
112         };
113         redisForPubsub = lib.mkOption {
114           type = lib.types.nullOr redisType;
115           default = null;
116           description = "`ioredis` options for pubsub. See [`README`](https://github.com/redis/ioredis?tab=readme-ov-file#connect-to-redis) for reference.";
117         };
118         redisForJobQueue = lib.mkOption {
119           type = lib.types.nullOr redisType;
120           default = null;
121           description = "`ioredis` options for the job queue. See [`README`](https://github.com/redis/ioredis?tab=readme-ov-file#connect-to-redis) for reference.";
122         };
123         redisForTimelines = lib.mkOption {
124           type = lib.types.nullOr redisType;
125           default = null;
126           description = "`ioredis` options for timelines. See [`README`](https://github.com/redis/ioredis?tab=readme-ov-file#connect-to-redis) for reference.";
127         };
128         meilisearch = lib.mkOption {
129           description = "Meilisearch connection options.";
130           type = lib.types.nullOr (
131             lib.types.submodule {
132               options = {
133                 host = lib.mkOption {
134                   type = lib.types.str;
135                   default = "localhost";
136                   description = "The Meilisearch host.";
137                 };
138                 port = lib.mkOption {
139                   type = lib.types.port;
140                   default = 7700;
141                   description = "The Meilisearch port.";
142                 };
143                 apiKey = lib.mkOption {
144                   type = lib.types.nullOr lib.types.str;
145                   default = null;
146                   description = "The Meilisearch API key.";
147                 };
148                 ssl = lib.mkOption {
149                   type = lib.types.bool;
150                   default = false;
151                   description = "Whether to connect via SSL.";
152                 };
153                 index = lib.mkOption {
154                   type = lib.types.nullOr lib.types.str;
155                   default = null;
156                   description = "Meilisearch index to use.";
157                 };
158                 scope = lib.mkOption {
159                   type = lib.types.enum [
160                     "local"
161                     "global"
162                   ];
163                   default = "local";
164                   description = "The search scope.";
165                 };
166               };
167             }
168           );
169           default = null;
170         };
171         id = lib.mkOption {
172           type = lib.types.enum [
173             "aid"
174             "aidx"
175             "meid"
176             "ulid"
177             "objectid"
178           ];
179           default = "aidx";
180           description = "The ID generation method to use. Do not change after starting Misskey for the first time.";
181         };
182       };
183     };
184   };
188   options = {
189     services.misskey = {
190       enable = lib.mkEnableOption "misskey";
191       package = lib.mkPackageOption pkgs "misskey" { };
192       inherit settings;
193       database = {
194         createLocally = lib.mkOption {
195           type = lib.types.bool;
196           default = false;
197           description = "Create the PostgreSQL database locally. Sets `services.misskey.settings.db.{db,host,port,user,pass}`.";
198         };
199         passwordFile = lib.mkOption {
200           type = lib.types.nullOr lib.types.path;
201           default = null;
202           description = "The path to a file containing the database password. Sets `services.misskey.settings.db.pass`.";
203         };
204       };
205       redis = {
206         createLocally = lib.mkOption {
207           type = lib.types.bool;
208           default = false;
209           description = "Create and use a local Redis instance. Sets `services.misskey.settings.redis.host`.";
210         };
211         passwordFile = lib.mkOption {
212           type = lib.types.nullOr lib.types.path;
213           default = null;
214           description = "The path to a file containing the Redis password. Sets `services.misskey.settings.redis.pass`.";
215         };
216       };
217       meilisearch = {
218         createLocally = lib.mkOption {
219           type = lib.types.bool;
220           default = false;
221           description = "Create and use a local Meilisearch instance. Sets `services.misskey.settings.meilisearch.{host,port,ssl}`.";
222         };
223         keyFile = lib.mkOption {
224           type = lib.types.nullOr lib.types.path;
225           default = null;
226           description = "The path to a file containing the Meilisearch API key. Sets `services.misskey.settings.meilisearch.apiKey`.";
227         };
228       };
229       reverseProxy = {
230         enable = lib.mkEnableOption "a HTTP reverse proxy for Misskey";
231         webserver = lib.mkOption {
232           type = lib.types.attrTag {
233             nginx = lib.mkOption {
234               type = lib.types.submodule (import ../web-servers/nginx/vhost-options.nix);
235               default = { };
236               description = ''
237                 Extra configuration for the nginx virtual host of Misskey.
238                 Set to `{ }` to use the default configuration.
239               '';
240             };
241             caddy = lib.mkOption {
242               type = lib.types.submodule (
243                 import ../web-servers/caddy/vhost-options.nix { cfg = config.services.caddy; }
244               );
245               default = { };
246               description = ''
247                 Extra configuration for the caddy virtual host of Misskey.
248                 Set to `{ }` to use the default configuration.
249               '';
250             };
251           };
252           description = "The webserver to use as the reverse proxy.";
253         };
254         host = lib.mkOption {
255           type = lib.types.nullOr lib.types.str;
256           description = ''
257             The fully qualified domain name to bind to. Sets `services.misskey.settings.url`.
259             This is required when using `services.misskey.reverseProxy.enable = true`.
260           '';
261           example = "misskey.example.com";
262           default = null;
263         };
264         ssl = lib.mkOption {
265           type = lib.types.nullOr lib.types.bool;
266           description = ''
267             Whether to enable SSL for the reverse proxy. Sets `services.misskey.settings.url`.
269             This is required when using `services.misskey.reverseProxy.enable = true`.
270           '';
271           example = true;
272           default = null;
273         };
274       };
275     };
276   };
278   config = lib.mkIf cfg.enable {
279     assertions = [
280       {
281         assertion =
282           cfg.reverseProxy.enable -> ((cfg.reverseProxy.host != null) && (cfg.reverseProxy.ssl != null));
283         message = "`services.misskey.reverseProxy.enable` requires `services.misskey.reverseProxy.host` and `services.misskey.reverseProxy.ssl` to be set.";
284       }
285     ];
287     services.misskey.settings = lib.mkMerge [
288       (lib.mkIf cfg.database.createLocally {
289         db = {
290           db = lib.mkDefault "misskey";
291           # Use unix socket instead of localhost to allow PostgreSQL peer authentication,
292           # required for `services.postgresql.ensureUsers`
293           host = lib.mkDefault "/var/run/postgresql";
294           port = lib.mkDefault config.services.postgresql.settings.port;
295           user = lib.mkDefault "misskey";
296           pass = lib.mkDefault null;
297         };
298       })
299       (lib.mkIf (cfg.database.passwordFile != null) { db.pass = lib.mkDefault "@DATABASE_PASSWORD@"; })
300       (lib.mkIf cfg.redis.createLocally { redis.host = lib.mkDefault "localhost"; })
301       (lib.mkIf (cfg.redis.passwordFile != null) { redis.pass = lib.mkDefault "@REDIS_PASSWORD@"; })
302       (lib.mkIf cfg.meilisearch.createLocally {
303         meilisearch = {
304           host = lib.mkDefault "localhost";
305           port = lib.mkDefault config.services.meilisearch.listenPort;
306           ssl = lib.mkDefault false;
307         };
308       })
309       (lib.mkIf (cfg.meilisearch.keyFile != null) {
310         meilisearch.apiKey = lib.mkDefault "@MEILISEARCH_KEY@";
311       })
312       (lib.mkIf cfg.reverseProxy.enable {
313         url = lib.mkDefault "${
314           if cfg.reverseProxy.ssl then "https" else "http"
315         }://${cfg.reverseProxy.host}";
316       })
317     ];
319     systemd.services.misskey = {
320       after = [
321         "network-online.target"
322         "postgresql.service"
323       ];
324       wants = [ "network-online.target" ];
325       wantedBy = [ "multi-user.target" ];
326       environment = {
327         MISSKEY_CONFIG_YML = "/run/misskey/default.yml";
328       };
329       preStart =
330         ''
331           install -m 700 ${settingsFormat.generate "misskey-config.yml" cfg.settings} /run/misskey/default.yml
332         ''
333         + (lib.optionalString (cfg.database.passwordFile != null) ''
334           ${pkgs.replace-secret}/bin/replace-secret '@DATABASE_PASSWORD@' "${cfg.database.passwordFile}" /run/misskey/default.yml
335         '')
336         + (lib.optionalString (cfg.redis.passwordFile != null) ''
337           ${pkgs.replace-secret}/bin/replace-secret '@REDIS_PASSWORD@' "${cfg.redis.passwordFile}" /run/misskey/default.yml
338         '')
339         + (lib.optionalString (cfg.meilisearch.keyFile != null) ''
340           ${pkgs.replace-secret}/bin/replace-secret '@MEILISEARCH_KEY@' "${cfg.meilisearch.keyFile}" /run/misskey/default.yml
341         '');
342       serviceConfig = {
343         ExecStart = "${cfg.package}/bin/misskey migrateandstart";
344         RuntimeDirectory = "misskey";
345         RuntimeDirectoryMode = "700";
346         StateDirectory = "misskey";
347         StateDirectoryMode = "700";
348         TimeoutSec = 60;
349         DynamicUser = true;
350         User = "misskey";
351         LockPersonality = true;
352         PrivateDevices = true;
353         PrivateUsers = true;
354         ProtectClock = true;
355         ProtectControlGroups = true;
356         ProtectHome = true;
357         ProtectHostname = true;
358         ProtectKernelLogs = true;
359         ProtectProc = "invisible";
360         ProtectKernelModules = true;
361         ProtectKernelTunables = true;
362         RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX AF_NETLINK";
363       };
364     };
366     services.postgresql = lib.mkIf cfg.database.createLocally {
367       enable = true;
368       ensureDatabases = [ "misskey" ];
369       ensureUsers = [
370         {
371           name = "misskey";
372           ensureDBOwnership = true;
373         }
374       ];
375     };
377     services.redis.servers = lib.mkIf cfg.redis.createLocally {
378       misskey = {
379         enable = true;
380         port = cfg.settings.redis.port;
381       };
382     };
384     services.meilisearch = lib.mkIf cfg.meilisearch.createLocally { enable = true; };
386     services.caddy = lib.mkIf (cfg.reverseProxy.enable && cfg.reverseProxy.webserver ? caddy) {
387       enable = true;
388       virtualHosts.${cfg.settings.url} = lib.mkMerge [
389         cfg.reverseProxy.webserver.caddy
390         {
391           hostName = lib.mkDefault cfg.settings.url;
392           extraConfig = ''
393             reverse_proxy localhost:${toString cfg.settings.port}
394           '';
395         }
396       ];
397     };
399     services.nginx = lib.mkIf (cfg.reverseProxy.enable && cfg.reverseProxy.webserver ? nginx) {
400       enable = true;
401       virtualHosts.${cfg.reverseProxy.host} = lib.mkMerge [
402         cfg.reverseProxy.webserver.nginx
403         {
404           locations."/" = {
405             proxyPass = lib.mkDefault "http://localhost:${toString cfg.settings.port}";
406             proxyWebsockets = lib.mkDefault true;
407             recommendedProxySettings = lib.mkDefault true;
408           };
409         }
410         (lib.mkIf (cfg.reverseProxy.ssl != null) { forceSSL = lib.mkDefault cfg.reverseProxy.ssl; })
411       ];
412     };
413   };
415   meta = {
416     maintainers = [ lib.maintainers.feathecutie ];
417   };