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