1{
2 config,
3 pkgs,
4 lib,
5 ...
6}:
7
8let
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.
40
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 };
185in
186
187{
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`.
258
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`.
268
269 This is required when using `services.misskey.reverseProxy.enable = true`.
270 '';
271 example = true;
272 default = null;
273 };
274 };
275 };
276 };
277
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 ];
286
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 ];
318
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 };
365
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 };
376
377 services.redis.servers = lib.mkIf cfg.redis.createLocally {
378 misskey = {
379 enable = true;
380 port = cfg.settings.redis.port;
381 };
382 };
383
384 services.meilisearch = lib.mkIf cfg.meilisearch.createLocally { enable = true; };
385
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 };
398
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 };
414
415 meta = {
416 maintainers = [ lib.maintainers.feathecutie ];
417 };
418}