1{ config, lib, pkgs, ... }:
2
3let
4 cfg = config.services.mastodon;
5 # We only want to create a database if we're actually going to connect to it.
6 databaseActuallyCreateLocally = cfg.database.createLocally && cfg.database.host == "/run/postgresql";
7
8 env = {
9 RAILS_ENV = "production";
10 NODE_ENV = "production";
11
12 LD_PRELOAD = "${pkgs.jemalloc}/lib/libjemalloc.so";
13
14 # mastodon-web concurrency.
15 WEB_CONCURRENCY = toString cfg.webProcesses;
16 MAX_THREADS = toString cfg.webThreads;
17
18 # mastodon-streaming concurrency.
19 STREAMING_CLUSTER_NUM = toString cfg.streamingProcesses;
20
21 DB_USER = cfg.database.user;
22
23 REDIS_HOST = cfg.redis.host;
24 REDIS_PORT = toString(cfg.redis.port);
25 DB_HOST = cfg.database.host;
26 DB_PORT = toString(cfg.database.port);
27 DB_NAME = cfg.database.name;
28 LOCAL_DOMAIN = cfg.localDomain;
29 SMTP_SERVER = cfg.smtp.host;
30 SMTP_PORT = toString(cfg.smtp.port);
31 SMTP_FROM_ADDRESS = cfg.smtp.fromAddress;
32 PAPERCLIP_ROOT_PATH = "/var/lib/mastodon/public-system";
33 PAPERCLIP_ROOT_URL = "/system";
34 ES_ENABLED = if (cfg.elasticsearch.host != null) then "true" else "false";
35 ES_HOST = cfg.elasticsearch.host;
36 ES_PORT = toString(cfg.elasticsearch.port);
37
38 TRUSTED_PROXY_IP = cfg.trustedProxy;
39 }
40 // (if cfg.smtp.authenticate then { SMTP_LOGIN = cfg.smtp.user; } else {})
41 // cfg.extraConfig;
42
43 systemCallsList = [ "@cpu-emulation" "@debug" "@keyring" "@ipc" "@mount" "@obsolete" "@privileged" "@setuid" ];
44
45 cfgService = {
46 # User and group
47 User = cfg.user;
48 Group = cfg.group;
49 # State directory and mode
50 StateDirectory = "mastodon";
51 StateDirectoryMode = "0750";
52 # Logs directory and mode
53 LogsDirectory = "mastodon";
54 LogsDirectoryMode = "0750";
55 # Proc filesystem
56 ProcSubset = "pid";
57 ProtectProc = "invisible";
58 # Access write directories
59 UMask = "0027";
60 # Capabilities
61 CapabilityBoundingSet = "";
62 # Security
63 NoNewPrivileges = true;
64 # Sandboxing
65 ProtectSystem = "strict";
66 ProtectHome = true;
67 PrivateTmp = true;
68 PrivateDevices = true;
69 PrivateUsers = true;
70 ProtectClock = true;
71 ProtectHostname = true;
72 ProtectKernelLogs = true;
73 ProtectKernelModules = true;
74 ProtectKernelTunables = true;
75 ProtectControlGroups = true;
76 RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" ];
77 RestrictNamespaces = true;
78 LockPersonality = true;
79 MemoryDenyWriteExecute = false;
80 RestrictRealtime = true;
81 RestrictSUIDSGID = true;
82 RemoveIPC = true;
83 PrivateMounts = true;
84 # System Call Filtering
85 SystemCallArchitectures = "native";
86 };
87
88 envFile = pkgs.writeText "mastodon.env" (lib.concatMapStrings (s: s + "\n") (
89 (lib.concatLists (lib.mapAttrsToList (name: value:
90 if value != null then [
91 "${name}=\"${toString value}\""
92 ] else []
93 ) env))));
94
95 mastodonEnv = pkgs.writeShellScriptBin "mastodon-env" ''
96 set -a
97 export RAILS_ROOT="${cfg.package}"
98 source "${envFile}"
99 source /var/lib/mastodon/.secrets_env
100 eval -- "\$@"
101 '';
102
103in {
104
105 options = {
106 services.mastodon = {
107 enable = lib.mkEnableOption (lib.mdDoc "Mastodon, a federated social network server");
108
109 configureNginx = lib.mkOption {
110 description = lib.mdDoc ''
111 Configure nginx as a reverse proxy for mastodon.
112 Note that this makes some assumptions on your setup, and sets settings that will
113 affect other virtualHosts running on your nginx instance, if any.
114 Alternatively you can configure a reverse-proxy of your choice to serve these paths:
115
116 `/ -> $(nix-instantiate --eval '<nixpkgs>' -A mastodon.outPath)/public`
117
118 `/ -> 127.0.0.1:{{ webPort }} `(If there was no file in the directory above.)
119
120 `/system/ -> /var/lib/mastodon/public-system/`
121
122 `/api/v1/streaming/ -> 127.0.0.1:{{ streamingPort }}`
123
124 Make sure that websockets are forwarded properly. You might want to set up caching
125 of some requests. Take a look at mastodon's provided nginx configuration at
126 `https://github.com/mastodon/mastodon/blob/master/dist/nginx.conf`.
127 '';
128 type = lib.types.bool;
129 default = false;
130 };
131
132 user = lib.mkOption {
133 description = lib.mdDoc ''
134 User under which mastodon runs. If it is set to "mastodon",
135 that user will be created, otherwise it should be set to the
136 name of a user created elsewhere. In both cases,
137 `mastodon` and a package containing only
138 the shell script `mastodon-env` will be added to
139 the user's package set. To run a command from
140 `mastodon` such as `tootctl`
141 with the environment configured by this module use
142 `mastodon-env`, as in:
143
144 `mastodon-env tootctl accounts create newuser --email newuser@example.com`
145 '';
146 type = lib.types.str;
147 default = "mastodon";
148 };
149
150 group = lib.mkOption {
151 description = lib.mdDoc ''
152 Group under which mastodon runs.
153 '';
154 type = lib.types.str;
155 default = "mastodon";
156 };
157
158 streamingPort = lib.mkOption {
159 description = lib.mdDoc "TCP port used by the mastodon-streaming service.";
160 type = lib.types.port;
161 default = 55000;
162 };
163 streamingProcesses = lib.mkOption {
164 description = lib.mdDoc ''
165 Processes used by the mastodon-streaming service.
166 Defaults to the number of CPU cores minus one.
167 '';
168 type = lib.types.nullOr lib.types.int;
169 default = null;
170 };
171
172 webPort = lib.mkOption {
173 description = lib.mdDoc "TCP port used by the mastodon-web service.";
174 type = lib.types.port;
175 default = 55001;
176 };
177 webProcesses = lib.mkOption {
178 description = lib.mdDoc "Processes used by the mastodon-web service.";
179 type = lib.types.int;
180 default = 2;
181 };
182 webThreads = lib.mkOption {
183 description = lib.mdDoc "Threads per process used by the mastodon-web service.";
184 type = lib.types.int;
185 default = 5;
186 };
187
188 sidekiqPort = lib.mkOption {
189 description = lib.mdDoc "TCP port used by the mastodon-sidekiq service.";
190 type = lib.types.port;
191 default = 55002;
192 };
193 sidekiqThreads = lib.mkOption {
194 description = lib.mdDoc "Worker threads used by the mastodon-sidekiq service.";
195 type = lib.types.int;
196 default = 25;
197 };
198
199 vapidPublicKeyFile = lib.mkOption {
200 description = lib.mdDoc ''
201 Path to file containing the public key used for Web Push
202 Voluntary Application Server Identification. A new keypair can
203 be generated by running:
204
205 `nix build -f '<nixpkgs>' mastodon; cd result; bin/rake webpush:generate_keys`
206
207 If {option}`mastodon.vapidPrivateKeyFile`does not
208 exist, it and this file will be created with a new keypair.
209 '';
210 default = "/var/lib/mastodon/secrets/vapid-public-key";
211 type = lib.types.str;
212 };
213
214 localDomain = lib.mkOption {
215 description = lib.mdDoc "The domain serving your Mastodon instance.";
216 example = "social.example.org";
217 type = lib.types.str;
218 };
219
220 secretKeyBaseFile = lib.mkOption {
221 description = lib.mdDoc ''
222 Path to file containing the secret key base.
223 A new secret key base can be generated by running:
224
225 `nix build -f '<nixpkgs>' mastodon; cd result; bin/rake secret`
226
227 If this file does not exist, it will be created with a new secret key base.
228 '';
229 default = "/var/lib/mastodon/secrets/secret-key-base";
230 type = lib.types.str;
231 };
232
233 otpSecretFile = lib.mkOption {
234 description = lib.mdDoc ''
235 Path to file containing the OTP secret.
236 A new OTP secret can be generated by running:
237
238 `nix build -f '<nixpkgs>' mastodon; cd result; bin/rake secret`
239
240 If this file does not exist, it will be created with a new OTP secret.
241 '';
242 default = "/var/lib/mastodon/secrets/otp-secret";
243 type = lib.types.str;
244 };
245
246 vapidPrivateKeyFile = lib.mkOption {
247 description = lib.mdDoc ''
248 Path to file containing the private key used for Web Push
249 Voluntary Application Server Identification. A new keypair can
250 be generated by running:
251
252 `nix build -f '<nixpkgs>' mastodon; cd result; bin/rake webpush:generate_keys`
253
254 If this file does not exist, it will be created with a new
255 private key.
256 '';
257 default = "/var/lib/mastodon/secrets/vapid-private-key";
258 type = lib.types.str;
259 };
260
261 trustedProxy = lib.mkOption {
262 description = lib.mdDoc ''
263 You need to set it to the IP from which your reverse proxy sends requests to Mastodon's web process,
264 otherwise Mastodon will record the reverse proxy's own IP as the IP of all requests, which would be
265 bad because IP addresses are used for important rate limits and security functions.
266 '';
267 type = lib.types.str;
268 default = "127.0.0.1";
269 };
270
271 enableUnixSocket = lib.mkOption {
272 description = lib.mdDoc ''
273 Instead of binding to an IP address like 127.0.0.1, you may bind to a Unix socket. This variable
274 is process-specific, e.g. you need different values for every process, and it works for both web (Puma)
275 processes and streaming API (Node.js) processes.
276 '';
277 type = lib.types.bool;
278 default = true;
279 };
280
281 redis = {
282 createLocally = lib.mkOption {
283 description = lib.mdDoc "Configure local Redis server for Mastodon.";
284 type = lib.types.bool;
285 default = true;
286 };
287
288 host = lib.mkOption {
289 description = lib.mdDoc "Redis host.";
290 type = lib.types.str;
291 default = "127.0.0.1";
292 };
293
294 port = lib.mkOption {
295 description = lib.mdDoc "Redis port.";
296 type = lib.types.port;
297 default = 31637;
298 };
299 };
300
301 database = {
302 createLocally = lib.mkOption {
303 description = lib.mdDoc "Configure local PostgreSQL database server for Mastodon.";
304 type = lib.types.bool;
305 default = true;
306 };
307
308 host = lib.mkOption {
309 type = lib.types.str;
310 default = "/run/postgresql";
311 example = "192.168.23.42";
312 description = lib.mdDoc "Database host address or unix socket.";
313 };
314
315 port = lib.mkOption {
316 type = lib.types.int;
317 default = 5432;
318 description = lib.mdDoc "Database host port.";
319 };
320
321 name = lib.mkOption {
322 type = lib.types.str;
323 default = "mastodon";
324 description = lib.mdDoc "Database name.";
325 };
326
327 user = lib.mkOption {
328 type = lib.types.str;
329 default = "mastodon";
330 description = lib.mdDoc "Database user.";
331 };
332
333 passwordFile = lib.mkOption {
334 type = lib.types.nullOr lib.types.path;
335 default = "/var/lib/mastodon/secrets/db-password";
336 example = "/run/keys/mastodon-db-password";
337 description = lib.mdDoc ''
338 A file containing the password corresponding to
339 {option}`database.user`.
340 '';
341 };
342 };
343
344 smtp = {
345 createLocally = lib.mkOption {
346 description = lib.mdDoc "Configure local Postfix SMTP server for Mastodon.";
347 type = lib.types.bool;
348 default = true;
349 };
350
351 authenticate = lib.mkOption {
352 description = lib.mdDoc "Authenticate with the SMTP server using username and password.";
353 type = lib.types.bool;
354 default = false;
355 };
356
357 host = lib.mkOption {
358 description = lib.mdDoc "SMTP host used when sending emails to users.";
359 type = lib.types.str;
360 default = "127.0.0.1";
361 };
362
363 port = lib.mkOption {
364 description = lib.mdDoc "SMTP port used when sending emails to users.";
365 type = lib.types.port;
366 default = 25;
367 };
368
369 fromAddress = lib.mkOption {
370 description = lib.mdDoc ''"From" address used when sending Emails to users.'';
371 type = lib.types.str;
372 };
373
374 user = lib.mkOption {
375 description = lib.mdDoc "SMTP login name.";
376 type = lib.types.str;
377 };
378
379 passwordFile = lib.mkOption {
380 description = lib.mdDoc ''
381 Path to file containing the SMTP password.
382 '';
383 default = "/var/lib/mastodon/secrets/smtp-password";
384 example = "/run/keys/mastodon-smtp-password";
385 type = lib.types.str;
386 };
387 };
388
389 elasticsearch = {
390 host = lib.mkOption {
391 description = lib.mdDoc ''
392 Elasticsearch host.
393 If it is not null, Elasticsearch full text search will be enabled.
394 '';
395 type = lib.types.nullOr lib.types.str;
396 default = null;
397 };
398
399 port = lib.mkOption {
400 description = lib.mdDoc "Elasticsearch port.";
401 type = lib.types.port;
402 default = 9200;
403 };
404 };
405
406 package = lib.mkOption {
407 type = lib.types.package;
408 default = pkgs.mastodon;
409 defaultText = lib.literalExpression "pkgs.mastodon";
410 description = lib.mdDoc "Mastodon package to use.";
411 };
412
413 extraConfig = lib.mkOption {
414 type = lib.types.attrs;
415 default = {};
416 description = lib.mdDoc ''
417 Extra environment variables to pass to all mastodon services.
418 '';
419 };
420
421 automaticMigrations = lib.mkOption {
422 type = lib.types.bool;
423 default = true;
424 description = lib.mdDoc ''
425 Do automatic database migrations.
426 '';
427 };
428
429 mediaAutoRemove = {
430 enable = lib.mkOption {
431 type = lib.types.bool;
432 default = true;
433 example = false;
434 description = lib.mdDoc ''
435 Automatically remove remote media attachments and preview cards older than the configured amount of days.
436
437 Recommended in https://docs.joinmastodon.org/admin/setup/.
438 '';
439 };
440
441 startAt = lib.mkOption {
442 type = lib.types.str;
443 default = "daily";
444 example = "hourly";
445 description = lib.mdDoc ''
446 How often to remove remote media.
447
448 The format is described in {manpage}`systemd.time(7)`.
449 '';
450 };
451
452 olderThanDays = lib.mkOption {
453 type = lib.types.int;
454 default = 30;
455 example = 14;
456 description = lib.mdDoc ''
457 How old remote media needs to be in order to be removed.
458 '';
459 };
460 };
461 };
462 };
463
464 config = lib.mkIf cfg.enable {
465 assertions = [
466 {
467 assertion = databaseActuallyCreateLocally -> (cfg.user == cfg.database.user);
468 message = ''For local automatic database provisioning (services.mastodon.database.createLocally == true) with peer authentication (services.mastodon.database.host == "/run/postgresql") to work services.mastodon.user and services.mastodon.database.user must be identical.'';
469 }
470 ];
471
472 systemd.services.mastodon-init-dirs = {
473 script = ''
474 umask 077
475
476 if ! test -f ${cfg.secretKeyBaseFile}; then
477 mkdir -p $(dirname ${cfg.secretKeyBaseFile})
478 bin/rake secret > ${cfg.secretKeyBaseFile}
479 fi
480 if ! test -f ${cfg.otpSecretFile}; then
481 mkdir -p $(dirname ${cfg.otpSecretFile})
482 bin/rake secret > ${cfg.otpSecretFile}
483 fi
484 if ! test -f ${cfg.vapidPrivateKeyFile}; then
485 mkdir -p $(dirname ${cfg.vapidPrivateKeyFile}) $(dirname ${cfg.vapidPublicKeyFile})
486 keypair=$(bin/rake webpush:generate_keys)
487 echo $keypair | grep --only-matching "Private -> [^ ]\+" | sed 's/^Private -> //' > ${cfg.vapidPrivateKeyFile}
488 echo $keypair | grep --only-matching "Public -> [^ ]\+" | sed 's/^Public -> //' > ${cfg.vapidPublicKeyFile}
489 fi
490
491 cat > /var/lib/mastodon/.secrets_env <<EOF
492 SECRET_KEY_BASE="$(cat ${cfg.secretKeyBaseFile})"
493 OTP_SECRET="$(cat ${cfg.otpSecretFile})"
494 VAPID_PRIVATE_KEY="$(cat ${cfg.vapidPrivateKeyFile})"
495 VAPID_PUBLIC_KEY="$(cat ${cfg.vapidPublicKeyFile})"
496 DB_PASS="$(cat ${cfg.database.passwordFile})"
497 '' + (if cfg.smtp.authenticate then ''
498 SMTP_PASSWORD="$(cat ${cfg.smtp.passwordFile})"
499 '' else "") + ''
500 EOF
501 '';
502 environment = env;
503 serviceConfig = {
504 Type = "oneshot";
505 WorkingDirectory = cfg.package;
506 # System Call Filtering
507 SystemCallFilter = [ ("~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ])) "@chown" "pipe" "pipe2" ];
508 } // cfgService;
509
510 after = [ "network.target" ];
511 };
512
513 systemd.services.mastodon-init-db = lib.mkIf cfg.automaticMigrations {
514 script = ''
515 if [ `psql ${cfg.database.name} -c \
516 "select count(*) from pg_class c \
517 join pg_namespace s on s.oid = c.relnamespace \
518 where s.nspname not in ('pg_catalog', 'pg_toast', 'information_schema') \
519 and s.nspname not like 'pg_temp%';" | sed -n 3p` -eq 0 ]; then
520 SAFETY_ASSURED=1 rails db:schema:load
521 rails db:seed
522 else
523 rails db:migrate
524 fi
525 '';
526 path = [ cfg.package pkgs.postgresql ];
527 environment = env;
528 serviceConfig = {
529 Type = "oneshot";
530 EnvironmentFile = "/var/lib/mastodon/.secrets_env";
531 WorkingDirectory = cfg.package;
532 # System Call Filtering
533 SystemCallFilter = [ ("~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ])) "@chown" "pipe" "pipe2" ];
534 } // cfgService;
535 after = [ "network.target" "mastodon-init-dirs.service" ]
536 ++ lib.optional databaseActuallyCreateLocally "postgresql.service";
537 requires = [ "mastodon-init-dirs.service" ]
538 ++ lib.optional databaseActuallyCreateLocally "postgresql.service";
539 };
540
541 systemd.services.mastodon-streaming = {
542 after = [ "network.target" "mastodon-init-dirs.service" ]
543 ++ lib.optional databaseActuallyCreateLocally "postgresql.service"
544 ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service";
545 requires = [ "mastodon-init-dirs.service" ]
546 ++ lib.optional databaseActuallyCreateLocally "postgresql.service"
547 ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service";
548 wantedBy = [ "multi-user.target" ];
549 description = "Mastodon streaming";
550 environment = env // (if cfg.enableUnixSocket
551 then { SOCKET = "/run/mastodon-streaming/streaming.socket"; }
552 else { PORT = toString(cfg.streamingPort); }
553 );
554 serviceConfig = {
555 ExecStart = "${cfg.package}/run-streaming.sh";
556 Restart = "always";
557 RestartSec = 20;
558 EnvironmentFile = "/var/lib/mastodon/.secrets_env";
559 WorkingDirectory = cfg.package;
560 # Runtime directory and mode
561 RuntimeDirectory = "mastodon-streaming";
562 RuntimeDirectoryMode = "0750";
563 # System Call Filtering
564 SystemCallFilter = [ ("~" + lib.concatStringsSep " " (systemCallsList ++ [ "@memlock" "@resources" ])) "pipe" "pipe2" ];
565 } // cfgService;
566 };
567
568 systemd.services.mastodon-web = {
569 after = [ "network.target" "mastodon-init-dirs.service" ]
570 ++ lib.optional databaseActuallyCreateLocally "postgresql.service"
571 ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service";
572 requires = [ "mastodon-init-dirs.service" ]
573 ++ lib.optional databaseActuallyCreateLocally "postgresql.service"
574 ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service";
575 wantedBy = [ "multi-user.target" ];
576 description = "Mastodon web";
577 environment = env // (if cfg.enableUnixSocket
578 then { SOCKET = "/run/mastodon-web/web.socket"; }
579 else { PORT = toString(cfg.webPort); }
580 );
581 serviceConfig = {
582 ExecStart = "${cfg.package}/bin/puma -C config/puma.rb";
583 Restart = "always";
584 RestartSec = 20;
585 EnvironmentFile = "/var/lib/mastodon/.secrets_env";
586 WorkingDirectory = cfg.package;
587 # Runtime directory and mode
588 RuntimeDirectory = "mastodon-web";
589 RuntimeDirectoryMode = "0750";
590 # System Call Filtering
591 SystemCallFilter = [ ("~" + lib.concatStringsSep " " systemCallsList) "@chown" "pipe" "pipe2" ];
592 } // cfgService;
593 path = with pkgs; [ file imagemagick ffmpeg ];
594 };
595
596 systemd.services.mastodon-sidekiq = {
597 after = [ "network.target" "mastodon-init-dirs.service" ]
598 ++ lib.optional databaseActuallyCreateLocally "postgresql.service"
599 ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service";
600 requires = [ "mastodon-init-dirs.service" ]
601 ++ lib.optional databaseActuallyCreateLocally "postgresql.service"
602 ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service";
603 wantedBy = [ "multi-user.target" ];
604 description = "Mastodon sidekiq";
605 environment = env // {
606 PORT = toString(cfg.sidekiqPort);
607 DB_POOL = toString cfg.sidekiqThreads;
608 };
609 serviceConfig = {
610 ExecStart = "${cfg.package}/bin/sidekiq -c ${toString cfg.sidekiqThreads} -r ${cfg.package}";
611 Restart = "always";
612 RestartSec = 20;
613 EnvironmentFile = "/var/lib/mastodon/.secrets_env";
614 WorkingDirectory = cfg.package;
615 # System Call Filtering
616 SystemCallFilter = [ ("~" + lib.concatStringsSep " " systemCallsList) "@chown" "pipe" "pipe2" ];
617 } // cfgService;
618 path = with pkgs; [ file imagemagick ffmpeg ];
619 };
620
621 systemd.services.mastodon-media-auto-remove = lib.mkIf cfg.mediaAutoRemove.enable {
622 description = "Mastodon media auto remove";
623 environment = env;
624 serviceConfig = {
625 Type = "oneshot";
626 EnvironmentFile = "/var/lib/mastodon/.secrets_env";
627 } // cfgService;
628 script = let
629 olderThanDays = toString cfg.mediaAutoRemove.olderThanDays;
630 in ''
631 ${cfg.package}/bin/tootctl media remove --days=${olderThanDays}
632 ${cfg.package}/bin/tootctl preview_cards remove --days=${olderThanDays}
633 '';
634 startAt = cfg.mediaAutoRemove.startAt;
635 };
636
637 services.nginx = lib.mkIf cfg.configureNginx {
638 enable = true;
639 recommendedProxySettings = true; # required for redirections to work
640 virtualHosts."${cfg.localDomain}" = {
641 root = "${cfg.package}/public/";
642 forceSSL = true; # mastodon only supports https
643 enableACME = true;
644
645 locations."/system/".alias = "/var/lib/mastodon/public-system/";
646
647 locations."/" = {
648 tryFiles = "$uri @proxy";
649 };
650
651 locations."@proxy" = {
652 proxyPass = (if cfg.enableUnixSocket then "http://unix:/run/mastodon-web/web.socket" else "http://127.0.0.1:${toString(cfg.webPort)}");
653 proxyWebsockets = true;
654 };
655
656 locations."/api/v1/streaming/" = {
657 proxyPass = (if cfg.enableUnixSocket then "http://unix:/run/mastodon-streaming/streaming.socket" else "http://127.0.0.1:${toString(cfg.streamingPort)}/");
658 proxyWebsockets = true;
659 };
660 };
661 };
662
663 services.postfix = lib.mkIf (cfg.smtp.createLocally && cfg.smtp.host == "127.0.0.1") {
664 enable = true;
665 hostname = lib.mkDefault "${cfg.localDomain}";
666 };
667 services.redis.servers.mastodon = lib.mkIf (cfg.redis.createLocally && cfg.redis.host == "127.0.0.1") {
668 enable = true;
669 port = cfg.redis.port;
670 bind = "127.0.0.1";
671 };
672 services.postgresql = lib.mkIf databaseActuallyCreateLocally {
673 enable = true;
674 ensureUsers = [
675 {
676 name = cfg.database.user;
677 ensurePermissions."DATABASE ${cfg.database.name}" = "ALL PRIVILEGES";
678 }
679 ];
680 ensureDatabases = [ cfg.database.name ];
681 };
682
683 users.users = lib.mkMerge [
684 (lib.mkIf (cfg.user == "mastodon") {
685 mastodon = {
686 isSystemUser = true;
687 home = cfg.package;
688 inherit (cfg) group;
689 };
690 })
691 (lib.attrsets.setAttrByPath [ cfg.user "packages" ] [ cfg.package mastodonEnv pkgs.imagemagick ])
692 ];
693
694 users.groups.${cfg.group}.members = lib.optional cfg.configureNginx config.services.nginx.user;
695 };
696
697 meta.maintainers = with lib.maintainers; [ happy-river erictapen ];
698
699}