1{
2 lib,
3 pkgs,
4 config,
5 options,
6 ...
7}:
8
9let
10 cfg = config.services.mastodon;
11 opt = options.services.mastodon;
12
13 # We only want to create a Redis and PostgreSQL databases if we're actually going to connect to it local.
14 redisActuallyCreateLocally =
15 cfg.redis.createLocally && (cfg.redis.host == "127.0.0.1" || cfg.redis.enableUnixSocket);
16 databaseActuallyCreateLocally =
17 cfg.database.createLocally && cfg.database.host == "/run/postgresql";
18
19 env = {
20 RAILS_ENV = "production";
21 NODE_ENV = "production";
22
23 BOOTSNAP_CACHE_DIR = "/var/cache/mastodon/precompile";
24 LD_PRELOAD = "${pkgs.jemalloc}/lib/libjemalloc.so";
25
26 # Concurrency mastodon-web
27 WEB_CONCURRENCY = toString cfg.webProcesses;
28 MAX_THREADS = toString cfg.webThreads;
29
30 DB_USER = cfg.database.user;
31
32 DB_HOST = cfg.database.host;
33 DB_NAME = cfg.database.name;
34 LOCAL_DOMAIN = cfg.localDomain;
35 SMTP_SERVER = cfg.smtp.host;
36 SMTP_PORT = toString cfg.smtp.port;
37 SMTP_FROM_ADDRESS = cfg.smtp.fromAddress;
38 PAPERCLIP_ROOT_PATH = "/var/lib/mastodon/public-system";
39 PAPERCLIP_ROOT_URL = "/system";
40 ES_ENABLED = if (cfg.elasticsearch.host != null) then "true" else "false";
41
42 TRUSTED_PROXY_IP = cfg.trustedProxy;
43 }
44 // lib.optionalAttrs (cfg.redis.host != null) { REDIS_HOST = cfg.redis.host; }
45 // lib.optionalAttrs (cfg.redis.port != null) { REDIS_PORT = toString cfg.redis.port; }
46 // lib.optionalAttrs (cfg.redis.createLocally && cfg.redis.enableUnixSocket) {
47 REDIS_URL = "unix://${config.services.redis.servers.mastodon.unixSocket}";
48 }
49 // lib.optionalAttrs (cfg.database.host != "/run/postgresql" && cfg.database.port != null) {
50 DB_PORT = toString cfg.database.port;
51 }
52 // lib.optionalAttrs cfg.smtp.authenticate { SMTP_LOGIN = cfg.smtp.user; }
53 // lib.optionalAttrs (cfg.elasticsearch.host != null) { ES_HOST = cfg.elasticsearch.host; }
54 // lib.optionalAttrs (cfg.elasticsearch.host != null) { ES_PORT = toString cfg.elasticsearch.port; }
55 // lib.optionalAttrs (cfg.elasticsearch.host != null && cfg.elasticsearch.prefix != null) {
56 ES_PREFIX = cfg.elasticsearch.prefix;
57 }
58 // lib.optionalAttrs (cfg.elasticsearch.host != null) { ES_PRESET = cfg.elasticsearch.preset; }
59 // lib.optionalAttrs (cfg.elasticsearch.user != null) { ES_USER = cfg.elasticsearch.user; }
60 // cfg.extraConfig;
61
62 systemCallsList = [
63 "@cpu-emulation"
64 "@debug"
65 "@keyring"
66 "@ipc"
67 "@mount"
68 "@obsolete"
69 "@privileged"
70 "@setuid"
71 ];
72
73 cfgService = {
74 # User and group
75 User = cfg.user;
76 Group = cfg.group;
77 # Working directory
78 WorkingDirectory = cfg.package;
79 # Cache directory and mode
80 CacheDirectory = "mastodon";
81 CacheDirectoryMode = "0750";
82 # State directory and mode
83 StateDirectory = "mastodon";
84 StateDirectoryMode = "0750";
85 # Logs directory and mode
86 LogsDirectory = "mastodon";
87 LogsDirectoryMode = "0750";
88 # Proc filesystem
89 ProcSubset = "pid";
90 ProtectProc = "invisible";
91 # Access write directories
92 UMask = "0027";
93 # Capabilities
94 CapabilityBoundingSet = "";
95 # Security
96 NoNewPrivileges = true;
97 # Sandboxing
98 ProtectSystem = "strict";
99 ProtectHome = true;
100 PrivateTmp = true;
101 PrivateDevices = true;
102 PrivateUsers = true;
103 ProtectClock = true;
104 ProtectHostname = true;
105 ProtectKernelLogs = true;
106 ProtectKernelModules = true;
107 ProtectKernelTunables = true;
108 ProtectControlGroups = true;
109 RestrictAddressFamilies = [
110 "AF_UNIX"
111 "AF_INET"
112 "AF_INET6"
113 "AF_NETLINK"
114 ];
115 RestrictNamespaces = true;
116 LockPersonality = true;
117 MemoryDenyWriteExecute = false;
118 RestrictRealtime = true;
119 RestrictSUIDSGID = true;
120 RemoveIPC = true;
121 PrivateMounts = true;
122 # System Call Filtering
123 SystemCallArchitectures = "native";
124 };
125
126 # Units that all Mastodon units After= and Requires= on
127 commonUnits =
128 lib.optional redisActuallyCreateLocally "redis-mastodon.service"
129 ++ lib.optional databaseActuallyCreateLocally "postgresql.target"
130 ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service";
131
132 envFile = pkgs.writeText "mastodon.env" (
133 lib.concatMapStrings (s: s + "\n") (
134 (lib.concatLists (
135 lib.mapAttrsToList (name: value: lib.optional (value != null) ''${name}="${toString value}"'') env
136 ))
137 )
138 );
139
140 mastodonTootctl =
141 let
142 sourceExtraEnv = lib.concatMapStrings (p: "source ${p}\n") cfg.extraEnvFiles;
143 in
144 pkgs.writeShellScriptBin "mastodon-tootctl" ''
145 set -a
146 export RAILS_ROOT="${cfg.package}"
147 source "${envFile}"
148 source /var/lib/mastodon/.secrets_env
149 ${sourceExtraEnv}
150
151 sudo=exec
152 if [[ "$USER" != ${cfg.user} ]]; then
153 sudo='exec /run/wrappers/bin/sudo -u ${cfg.user} --preserve-env'
154 fi
155 $sudo ${cfg.package}/bin/tootctl "$@"
156 '';
157
158 sidekiqUnits = lib.attrsets.mapAttrs' (
159 name: processCfg:
160 lib.nameValuePair "mastodon-sidekiq-${name}" (
161 let
162 jobClassArgs = toString (builtins.map (c: "-q ${c}") processCfg.jobClasses);
163 jobClassLabel = toString ([ "" ] ++ processCfg.jobClasses);
164 threads = toString (if processCfg.threads == null then cfg.sidekiqThreads else processCfg.threads);
165 in
166 {
167 after = [
168 "network.target"
169 "mastodon-init-dirs.service"
170 ]
171 ++ commonUnits;
172 requires = [ "mastodon-init-dirs.service" ] ++ commonUnits;
173 description = "Mastodon sidekiq${jobClassLabel}";
174 wantedBy = [ "mastodon.target" ];
175 environment = env // {
176 PORT = toString cfg.sidekiqPort;
177 DB_POOL = threads;
178 };
179 serviceConfig = {
180 ExecStart = "${cfg.package}/bin/sidekiq ${jobClassArgs} -c ${threads} -r ${cfg.package}";
181 Restart = "always";
182 RestartSec = 20;
183 EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ] ++ cfg.extraEnvFiles;
184 WorkingDirectory = cfg.package;
185 LimitNOFILE = "1024000";
186 # System Call Filtering
187 SystemCallFilter = [
188 ("~" + lib.concatStringsSep " " systemCallsList)
189 "@chown"
190 "pipe"
191 "pipe2"
192 ];
193 }
194 // cfgService;
195 path = with pkgs; [
196 ffmpeg-headless
197 file
198 ];
199 }
200 )
201 ) cfg.sidekiqProcesses;
202
203 streamingUnits = builtins.listToAttrs (
204 map (i: {
205 name = "mastodon-streaming-${toString i}";
206 value = {
207 after = [
208 "network.target"
209 "mastodon-init-dirs.service"
210 ]
211 ++ commonUnits;
212 requires = [ "mastodon-init-dirs.service" ] ++ commonUnits;
213 wantedBy = [
214 "mastodon.target"
215 "mastodon-streaming.target"
216 ];
217 description = "Mastodon streaming ${toString i}";
218 environment = env // {
219 SOCKET = "/run/mastodon-streaming/streaming-${toString i}.socket";
220 };
221 serviceConfig = {
222 ExecStart = "${cfg.package}/run-streaming.sh";
223 Restart = "always";
224 RestartSec = 20;
225 EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ] ++ cfg.extraEnvFiles;
226 WorkingDirectory = cfg.package;
227 # Runtime directory and mode
228 RuntimeDirectory = "mastodon-streaming";
229 RuntimeDirectoryMode = "0750";
230 # System Call Filtering
231 SystemCallFilter = [
232 (
233 "~"
234 + lib.concatStringsSep " " (
235 systemCallsList
236 ++ [
237 "@memlock"
238 "@resources"
239 ]
240 )
241 )
242 "pipe"
243 "pipe2"
244 ];
245 }
246 // cfgService;
247 };
248 }) (lib.range 1 cfg.streamingProcesses)
249 );
250
251in
252{
253
254 imports = [
255 (lib.mkRemovedOptionModule [
256 "services"
257 "mastodon"
258 "streamingPort"
259 ] "Mastodon currently doesn't support streaming via TCP ports. Please open a PR if you need this.")
260 (lib.mkRemovedOptionModule [
261 "services"
262 "mastodon"
263 "otpSecretFile"
264 ] "The OTP_SECRET option was removed from Mastodon in version 4.4.0")
265 ];
266
267 options = {
268 services.mastodon = {
269 enable = lib.mkEnableOption "Mastodon, a federated social network server";
270
271 configureNginx = lib.mkOption {
272 description = ''
273 Configure nginx as a reverse proxy for mastodon.
274 Note that this makes some assumptions on your setup, and sets settings that will
275 affect other virtualHosts running on your nginx instance, if any.
276 Alternatively you can configure a reverse-proxy of your choice to serve these paths:
277
278 `/ -> ''${pkgs.mastodon}/public`
279
280 `/ -> 127.0.0.1:{{ webPort }} `(If there was no file in the directory above.)
281
282 `/system/ -> /var/lib/mastodon/public-system/`
283
284 `/api/v1/streaming/ -> 127.0.0.1:{{ streamingPort }}`
285
286 Make sure that websockets are forwarded properly. You might want to set up caching
287 of some requests. Take a look at mastodon's provided nginx configuration at
288 `https://github.com/mastodon/mastodon/blob/master/dist/nginx.conf`.
289 '';
290 type = lib.types.bool;
291 default = false;
292 };
293
294 user = lib.mkOption {
295 description = ''
296 User under which mastodon runs. If it is set to "mastodon",
297 that user will be created, otherwise it should be set to the
298 name of a user created elsewhere.
299 In both cases, the `mastodon` package will be added to the user's package set
300 and a tootctl wrapper to system packages that switches to the configured account
301 and load the right environment.
302 '';
303 type = lib.types.str;
304 default = "mastodon";
305 };
306
307 group = lib.mkOption {
308 description = ''
309 Group under which mastodon runs.
310 '';
311 type = lib.types.str;
312 default = "mastodon";
313 };
314
315 streamingProcesses = lib.mkOption {
316 description = ''
317 Number of processes used by the mastodon-streaming service.
318 Please define this explicitly, recommended is the amount of your CPU cores minus one.
319 '';
320 type = lib.types.ints.positive;
321 example = 3;
322 };
323
324 webPort = lib.mkOption {
325 description = "TCP port used by the mastodon-web service.";
326 type = lib.types.port;
327 default = 55001;
328 };
329 webProcesses = lib.mkOption {
330 description = "Processes used by the mastodon-web service.";
331 type = lib.types.int;
332 default = 2;
333 };
334 webThreads = lib.mkOption {
335 description = "Threads per process used by the mastodon-web service.";
336 type = lib.types.int;
337 default = 5;
338 };
339
340 sidekiqPort = lib.mkOption {
341 description = "TCP port used by the mastodon-sidekiq service.";
342 type = lib.types.port;
343 default = 55002;
344 };
345
346 sidekiqThreads = lib.mkOption {
347 description = "Worker threads used by the mastodon-sidekiq-all service. If `sidekiqProcesses` is configured and any processes specify null `threads`, this value is used.";
348 type = lib.types.int;
349 default = 25;
350 };
351
352 sidekiqProcesses = lib.mkOption {
353 description = "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!*";
354 type =
355 with lib.types;
356 attrsOf (submodule {
357 options = {
358 jobClasses = lib.mkOption {
359 type = listOf (enum [
360 "default"
361 "fasp"
362 "push"
363 "pull"
364 "mailers"
365 "scheduler"
366 "ingress"
367 ]);
368 description = "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.*";
369 };
370 threads = lib.mkOption {
371 type = nullOr int;
372 description = "Number of threads this process should use for executing jobs. If null, the configured `sidekiqThreads` are used.";
373 };
374 };
375 });
376 default = {
377 all = {
378 jobClasses = [ ];
379 threads = null;
380 };
381 };
382 example = {
383 all = {
384 jobClasses = [ ];
385 threads = null;
386 };
387 ingress = {
388 jobClasses = [ "ingress" ];
389 threads = 5;
390 };
391 default = {
392 jobClasses = [ "default" ];
393 threads = 10;
394 };
395 push-pull = {
396 jobClasses = [
397 "push"
398 "pull"
399 ];
400 threads = 5;
401 };
402 };
403 };
404
405 vapidPublicKeyFile = lib.mkOption {
406 description = ''
407 Path to file containing the public key used for Web Push
408 Voluntary Application Server Identification. A new keypair can
409 be generated by running:
410
411 `nix build -f '<nixpkgs>' mastodon; cd result; RAILS_ENV=production bin/rake webpush:generate_keys`
412
413 If {option}`mastodon.vapidPrivateKeyFile`does not
414 exist, it and this file will be created with a new keypair.
415 '';
416 default = "/var/lib/mastodon/secrets/vapid-public-key";
417 type = lib.types.str;
418 };
419
420 vapidPrivateKeyFile = lib.mkOption {
421 description = ''
422 Path to file containing the private key used for Web Push
423 Voluntary Application Server Identification. A new keypair can
424 be generated by running:
425
426 `nix build -f '<nixpkgs>' mastodon; cd result; bin/rake webpush:generate_keys`
427
428 If this file does not exist, it will be created with a new
429 private key.
430 '';
431 default = "/var/lib/mastodon/secrets/vapid-private-key";
432 type = lib.types.str;
433 };
434
435 localDomain = lib.mkOption {
436 description = "The domain serving your Mastodon instance.";
437 example = "social.example.org";
438 type = lib.types.str;
439 };
440
441 activeRecordEncryptionDeterministicKeyFile = lib.mkOption {
442 description = ''
443 This key must be set to enable the Active Record Encryption feature within
444 Rails that Mastodon uses to encrypt and decrypt some database attributes.
445 A new Active Record keys can be generated by running:
446
447 `nix build -f '<nixpkgs>' mastodon; cd result; RAILS_ENV=production ./bin/rails db:encryption:init`
448
449 If this file does not exist, it will be created with a new Active Record
450 keys.
451 '';
452 default = "/var/lib/mastodon/secrets/active-record-encryption-deterministic-key";
453 type = lib.types.str;
454 };
455
456 activeRecordEncryptionKeyDerivationSaltFile = lib.mkOption {
457 description = ''
458 This key must be set to enable the Active Record Encryption feature within
459 Rails that Mastodon uses to encrypt and decrypt some database attributes.
460 A new Active Record keys can be generated by running:
461
462 `nix build -f '<nixpkgs>' mastodon; cd result; RAILS_ENV=production ./bin/rails db:encryption:init`
463
464 If this file does not exist, it will be created with a new Active Record
465 keys.
466 '';
467 default = "/var/lib/mastodon/secrets/active-record-encryption-key-derivation-salt";
468 type = lib.types.str;
469 };
470
471 activeRecordEncryptionPrimaryKeyFile = lib.mkOption {
472 description = ''
473 This key must be set to enable the Active Record Encryption feature within
474 Rails that Mastodon uses to encrypt and decrypt some database attributes.
475 A new Active Record keys can be generated by running:
476
477 `nix build -f '<nixpkgs>' mastodon; cd result; RAILS_ENV=production ./bin/rails db:encryption:init`
478
479 If this file does not exist, it will be created with a new Active Record
480 keys.
481 '';
482 default = "/var/lib/mastodon/secrets/active-record-encryption-primary-key";
483 type = lib.types.str;
484 };
485
486 secretKeyBaseFile = lib.mkOption {
487 description = ''
488 Path to file containing the secret key base.
489 A new secret key base can be generated by running:
490
491 `nix build -f '<nixpkgs>' mastodon; cd result; bin/bundle exec rails secret`
492
493 If this file does not exist, it will be created with a new secret key base.
494 '';
495 default = "/var/lib/mastodon/secrets/secret-key-base";
496 type = lib.types.str;
497 };
498
499 trustedProxy = lib.mkOption {
500 description = ''
501 You need to set it to the IP from which your reverse proxy sends requests to Mastodon's web process,
502 otherwise Mastodon will record the reverse proxy's own IP as the IP of all requests, which would be
503 bad because IP addresses are used for important rate limits and security functions.
504 '';
505 type = lib.types.str;
506 default = "127.0.0.1";
507 };
508
509 enableUnixSocket = lib.mkOption {
510 description = ''
511 Instead of binding to an IP address like 127.0.0.1, you may bind to a Unix socket. This variable
512 is process-specific, e.g. you need different values for every process, and it works for both web (Puma)
513 processes and streaming API (Node.js) processes.
514 '';
515 type = lib.types.bool;
516 default = true;
517 };
518
519 redis = {
520 createLocally = lib.mkOption {
521 description = "Configure local Redis server for Mastodon.";
522 type = lib.types.bool;
523 default = true;
524 };
525
526 host = lib.mkOption {
527 description = "Redis host.";
528 type = lib.types.nullOr lib.types.str;
529 default = if cfg.redis.createLocally && !cfg.redis.enableUnixSocket then "127.0.0.1" else null;
530 defaultText = lib.literalExpression ''
531 if config.${opt.redis.createLocally} && !config.${opt.redis.enableUnixSocket} then "127.0.0.1" else null
532 '';
533 };
534
535 port = lib.mkOption {
536 description = "Redis port.";
537 type = lib.types.nullOr lib.types.port;
538 default = if cfg.redis.createLocally && !cfg.redis.enableUnixSocket then 31637 else null;
539 defaultText = lib.literalExpression ''
540 if config.${opt.redis.createLocally} && !config.${opt.redis.enableUnixSocket} then 31637 else null
541 '';
542 };
543
544 passwordFile = lib.mkOption {
545 description = "A file containing the password for Redis database.";
546 type = lib.types.nullOr lib.types.path;
547 default = null;
548 example = "/run/keys/mastodon-redis-password";
549 };
550
551 enableUnixSocket = lib.mkOption {
552 description = "Use Unix socket";
553 type = lib.types.bool;
554 default = true;
555 };
556 };
557
558 database = {
559 createLocally = lib.mkOption {
560 description = "Configure local PostgreSQL database server for Mastodon.";
561 type = lib.types.bool;
562 default = true;
563 };
564
565 host = lib.mkOption {
566 type = lib.types.str;
567 default = "/run/postgresql";
568 example = "192.168.23.42";
569 description = "Database host address or unix socket.";
570 };
571
572 port = lib.mkOption {
573 type = lib.types.nullOr lib.types.port;
574 default = if cfg.database.createLocally then null else 5432;
575 defaultText = lib.literalExpression ''
576 if config.${opt.database.createLocally}
577 then null
578 else 5432
579 '';
580 description = "Database host port.";
581 };
582
583 name = lib.mkOption {
584 type = lib.types.str;
585 default = "mastodon";
586 description = "Database name.";
587 };
588
589 user = lib.mkOption {
590 type = lib.types.str;
591 default = "mastodon";
592 description = "Database user.";
593 };
594
595 passwordFile = lib.mkOption {
596 type = lib.types.nullOr lib.types.path;
597 default = null;
598 example = "/var/lib/mastodon/secrets/db-password";
599 description = ''
600 A file containing the password corresponding to
601 {option}`database.user`.
602 '';
603 };
604 };
605
606 smtp = {
607 createLocally = lib.mkOption {
608 description = "Configure local Postfix SMTP server for Mastodon.";
609 type = lib.types.bool;
610 default = true;
611 };
612
613 authenticate = lib.mkOption {
614 description = "Authenticate with the SMTP server using username and password.";
615 type = lib.types.bool;
616 default = false;
617 };
618
619 host = lib.mkOption {
620 description = "SMTP host used when sending emails to users.";
621 type = lib.types.str;
622 default = "127.0.0.1";
623 };
624
625 port = lib.mkOption {
626 description = "SMTP port used when sending emails to users.";
627 type = lib.types.port;
628 default = 25;
629 };
630
631 fromAddress = lib.mkOption {
632 description = ''"From" address used when sending Emails to users.'';
633 type = lib.types.str;
634 };
635
636 user = lib.mkOption {
637 type = lib.types.nullOr lib.types.str;
638 default = null;
639 example = "mastodon@example.com";
640 description = "SMTP login name.";
641 };
642
643 passwordFile = lib.mkOption {
644 type = lib.types.nullOr lib.types.path;
645 default = null;
646 example = "/var/lib/mastodon/secrets/smtp-password";
647 description = ''
648 Path to file containing the SMTP password.
649 '';
650 };
651 };
652
653 elasticsearch = {
654 host = lib.mkOption {
655 description = ''
656 Elasticsearch host.
657 If it is not null, Elasticsearch full text search will be enabled.
658 '';
659 type = lib.types.nullOr lib.types.str;
660 default = null;
661 };
662
663 port = lib.mkOption {
664 description = "Elasticsearch port.";
665 type = lib.types.port;
666 default = 9200;
667 };
668
669 prefix = lib.mkOption {
670 description = ''
671 If provided, adds a prefix to indexes in Elasticsearch. This allows to use the same
672 Elasticsearch cluster between different projects or Mastodon servers.
673 '';
674 type = lib.types.nullOr lib.types.str;
675 default = null;
676 example = "mastodon";
677 };
678
679 preset = lib.mkOption {
680 description = ''
681 It controls the ElasticSearch indices configuration (number of shards and replica).
682 '';
683 type = lib.types.enum [
684 "single_node_cluster"
685 "small_cluster"
686 "large_cluster"
687 ];
688 default = "single_node_cluster";
689 example = "large_cluster";
690 };
691
692 user = lib.mkOption {
693 description = "Used for optionally authenticating with Elasticsearch.";
694 type = lib.types.nullOr lib.types.str;
695 default = null;
696 example = "elasticsearch-mastodon";
697 };
698
699 passwordFile = lib.mkOption {
700 description = ''
701 Path to file containing password for optionally authenticating with Elasticsearch.
702 '';
703 type = lib.types.nullOr lib.types.path;
704 default = null;
705 example = "/var/lib/mastodon/secrets/elasticsearch-password";
706 };
707 };
708
709 package = lib.mkPackageOption pkgs "mastodon" { };
710
711 extraConfig = lib.mkOption {
712 type = lib.types.attrs;
713 default = { };
714 description = ''
715 Extra environment variables to pass to all mastodon services.
716 '';
717 };
718
719 extraEnvFiles = lib.mkOption {
720 type = with lib.types; listOf path;
721 default = [ ];
722 description = ''
723 Extra environment files to pass to all mastodon services. Useful for passing down environmental secrets.
724 '';
725 example = [ "/etc/mastodon/s3config.env" ];
726 };
727
728 automaticMigrations = lib.mkOption {
729 type = lib.types.bool;
730 default = true;
731 description = ''
732 Do automatic database migrations.
733 '';
734 };
735
736 mediaAutoRemove = {
737 enable = lib.mkOption {
738 type = lib.types.bool;
739 default = true;
740 example = false;
741 description = ''
742 Automatically remove remote media attachments and preview cards older than the configured amount of days.
743
744 Recommended in <https://docs.joinmastodon.org/admin/setup/>.
745 '';
746 };
747
748 startAt = lib.mkOption {
749 type = lib.types.str;
750 default = "daily";
751 example = "hourly";
752 description = ''
753 How often to remove remote media.
754
755 The format is described in {manpage}`systemd.time(7)`.
756 '';
757 };
758
759 olderThanDays = lib.mkOption {
760 type = lib.types.int;
761 default = 30;
762 example = 14;
763 description = ''
764 How old remote media needs to be in order to be removed.
765 '';
766 };
767 };
768 };
769 };
770
771 config = lib.mkIf cfg.enable (
772 lib.mkMerge [
773 {
774 assertions = [
775 {
776 assertion =
777 !redisActuallyCreateLocally -> (cfg.redis.host != "127.0.0.1" && cfg.redis.port != null);
778 message = ''
779 `services.mastodon.redis.host` and `services.mastodon.redis.port` need to be set if
780 `services.mastodon.redis.createLocally` is not enabled.
781 '';
782 }
783 {
784 assertion =
785 redisActuallyCreateLocally
786 -> (!cfg.redis.enableUnixSocket || (cfg.redis.host == null && cfg.redis.port == null));
787 message = ''
788 `services.mastodon.redis.enableUnixSocket` needs to be disabled if
789 `services.mastodon.redis.host` and `services.mastodon.redis.port` is used.
790 '';
791 }
792 {
793 assertion =
794 redisActuallyCreateLocally -> (!cfg.redis.enableUnixSocket || cfg.redis.passwordFile == null);
795 message = ''
796 <option>services.mastodon.redis.enableUnixSocket</option> needs to be disabled if
797 <option>services.mastodon.redis.passwordFile</option> is used.
798 '';
799 }
800 {
801 assertion =
802 databaseActuallyCreateLocally
803 -> (cfg.user == cfg.database.user && cfg.database.user == cfg.database.name);
804 message = ''
805 For local automatic database provisioning (services.mastodon.database.createLocally == true) with peer
806 authentication (services.mastodon.database.host == "/run/postgresql") to work services.mastodon.user
807 and services.mastodon.database.user must be identical.
808 '';
809 }
810 {
811 assertion = !databaseActuallyCreateLocally -> (cfg.database.host != "/run/postgresql");
812 message = ''
813 <option>services.mastodon.database.host</option> needs to be set if
814 <option>services.mastodon.database.createLocally</option> is not enabled.
815 '';
816 }
817 {
818 assertion = cfg.smtp.authenticate -> (cfg.smtp.user != null);
819 message = ''
820 <option>services.mastodon.smtp.user</option> needs to be set if
821 <option>services.mastodon.smtp.authenticate</option> is enabled.
822 '';
823 }
824 {
825 assertion = cfg.smtp.authenticate -> (cfg.smtp.passwordFile != null);
826 message = ''
827 <option>services.mastodon.smtp.passwordFile</option> needs to be set if
828 <option>services.mastodon.smtp.authenticate</option> is enabled.
829 '';
830 }
831 {
832 assertion =
833 1 == (lib.count (x: x) (
834 lib.mapAttrsToList (
835 _: v: builtins.elem "scheduler" v.jobClasses || v.jobClasses == [ ]
836 ) cfg.sidekiqProcesses
837 ));
838 message = "There must be exactly one Sidekiq queue in services.mastodon.sidekiqProcesses with jobClass \"scheduler\".";
839 }
840 ];
841
842 environment.systemPackages = [ mastodonTootctl ];
843
844 systemd.targets.mastodon = {
845 description = "Target for all Mastodon services";
846 wantedBy = [ "multi-user.target" ];
847 after = [ "network.target" ];
848 };
849
850 systemd.targets.mastodon-streaming = {
851 description = "Target for all Mastodon streaming services";
852 wantedBy = [
853 "multi-user.target"
854 "mastodon.target"
855 ];
856 after = [ "network.target" ];
857 };
858
859 systemd.services.mastodon-init-dirs = {
860 script = ''
861 umask 077
862
863 if ! test -d /var/cache/mastodon/precompile; then
864 ${cfg.package}/bin/bundle exec bootsnap precompile --gemfile ${cfg.package}/app ${cfg.package}/lib
865 fi
866 if ! test -f ${cfg.activeRecordEncryptionDeterministicKeyFile}; then
867 mkdir -p $(dirname ${cfg.activeRecordEncryptionDeterministicKeyFile})
868 bin/rails db:encryption:init | grep --only-matching "ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=[^ ]\+" | sed 's/^ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=//' > ${cfg.activeRecordEncryptionDeterministicKeyFile}
869 fi
870 if ! test -f ${cfg.activeRecordEncryptionKeyDerivationSaltFile}; then
871 mkdir -p $(dirname ${cfg.activeRecordEncryptionKeyDerivationSaltFile})
872 bin/rails db:encryption:init | grep --only-matching "ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=[^ ]\+" | sed 's/^ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=//' > ${cfg.activeRecordEncryptionKeyDerivationSaltFile}
873 fi
874 if ! test -f ${cfg.activeRecordEncryptionPrimaryKeyFile}; then
875 mkdir -p $(dirname ${cfg.activeRecordEncryptionPrimaryKeyFile})
876 bin/rails db:encryption:init | grep --only-matching "ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=[^ ]\+" | sed 's/^ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=//' > ${cfg.activeRecordEncryptionPrimaryKeyFile}
877 fi
878 if ! test -f ${cfg.secretKeyBaseFile}; then
879 mkdir -p $(dirname ${cfg.secretKeyBaseFile})
880 bin/bundle exec rails secret > ${cfg.secretKeyBaseFile}
881 fi
882 if ! test -f ${cfg.vapidPrivateKeyFile}; then
883 mkdir -p $(dirname ${cfg.vapidPrivateKeyFile}) $(dirname ${cfg.vapidPublicKeyFile})
884 keypair=$(bin/rake webpush:generate_keys)
885 echo $keypair | grep --only-matching "Private -> [^ ]\+" | sed 's/^Private -> //' > ${cfg.vapidPrivateKeyFile}
886 echo $keypair | grep --only-matching "Public -> [^ ]\+" | sed 's/^Public -> //' > ${cfg.vapidPublicKeyFile}
887 fi
888
889 cat > /var/lib/mastodon/.secrets_env <<EOF
890 ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY="$(cat ${cfg.activeRecordEncryptionDeterministicKeyFile})"
891 ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT="$(cat ${cfg.activeRecordEncryptionKeyDerivationSaltFile})"
892 ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY="$(cat ${cfg.activeRecordEncryptionPrimaryKeyFile})"
893 SECRET_KEY_BASE="$(cat ${cfg.secretKeyBaseFile})"
894 VAPID_PRIVATE_KEY="$(cat ${cfg.vapidPrivateKeyFile})"
895 VAPID_PUBLIC_KEY="$(cat ${cfg.vapidPublicKeyFile})"
896 ''
897 + lib.optionalString (cfg.redis.passwordFile != null) ''
898 REDIS_PASSWORD="$(cat ${cfg.redis.passwordFile})"
899 ''
900 + lib.optionalString (cfg.database.passwordFile != null) ''
901 DB_PASS="$(cat ${cfg.database.passwordFile})"
902 ''
903 + lib.optionalString cfg.smtp.authenticate ''
904 SMTP_PASSWORD="$(cat ${cfg.smtp.passwordFile})"
905 ''
906 + lib.optionalString (cfg.elasticsearch.passwordFile != null) ''
907 ES_PASS="$(cat ${cfg.elasticsearch.passwordFile})"
908 ''
909 + ''
910 EOF
911 '';
912 environment = env;
913 serviceConfig = {
914 Type = "oneshot";
915 SyslogIdentifier = "mastodon-init-dirs";
916 # System Call Filtering
917 SystemCallFilter = [
918 ("~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ]))
919 "@chown"
920 "pipe"
921 "pipe2"
922 ];
923 }
924 // cfgService;
925
926 after = [ "network.target" ];
927 };
928
929 systemd.services.mastodon-init-db = lib.mkIf cfg.automaticMigrations {
930 script =
931 lib.optionalString (!databaseActuallyCreateLocally) ''
932 umask 077
933 export PGPASSWORD="$(cat '${cfg.database.passwordFile}')"
934 ''
935 + ''
936 result="$(psql -t --csv -c \
937 "select count(*) from pg_class c \
938 join pg_namespace s on s.oid = c.relnamespace \
939 where s.nspname not in ('pg_catalog', 'pg_toast', 'information_schema') \
940 and s.nspname not like 'pg_temp%';")" || error_code=$?
941 if [ "''${error_code:-0}" -ne 0 ]; then
942 echo "Failure checking if database is seeded. psql gave exit code $error_code"
943 exit "$error_code"
944 fi
945 if [ "$result" -eq 0 ]; then
946 echo "Seeding database"
947 SAFETY_ASSURED=1 rails db:schema:load
948 rails db:seed
949 else
950 echo "Migrating database (this might be a noop)"
951 rails db:migrate
952 fi
953 ''
954 + lib.optionalString (!databaseActuallyCreateLocally) ''
955 unset PGPASSWORD
956 '';
957 path = [
958 cfg.package
959 (if databaseActuallyCreateLocally then config.services.postgresql.package else pkgs.postgresql)
960 ];
961 environment =
962 env
963 // lib.optionalAttrs (!databaseActuallyCreateLocally) {
964 PGHOST = cfg.database.host;
965 PGPORT = toString cfg.database.port;
966 PGDATABASE = cfg.database.name;
967 PGUSER = cfg.database.user;
968 };
969 serviceConfig = {
970 Type = "oneshot";
971 EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ] ++ cfg.extraEnvFiles;
972 WorkingDirectory = cfg.package;
973 # System Call Filtering
974 SystemCallFilter = [
975 ("~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ]))
976 "@chown"
977 "pipe"
978 "pipe2"
979 ];
980 }
981 // cfgService;
982 after = [
983 "network.target"
984 "mastodon-init-dirs.service"
985 ]
986 ++ lib.optional databaseActuallyCreateLocally "postgresql.target";
987 requires = [
988 "mastodon-init-dirs.service"
989 ]
990 ++ lib.optional databaseActuallyCreateLocally "postgresql.target";
991 };
992
993 systemd.services.mastodon-web = {
994 after = [
995 "network.target"
996 "mastodon-init-dirs.service"
997 ]
998 ++ commonUnits;
999 requires = [ "mastodon-init-dirs.service" ] ++ commonUnits;
1000 wantedBy = [ "mastodon.target" ];
1001 description = "Mastodon web";
1002 environment =
1003 env
1004 // (
1005 if cfg.enableUnixSocket then
1006 { SOCKET = "/run/mastodon-web/web.socket"; }
1007 else
1008 { PORT = toString cfg.webPort; }
1009 );
1010 serviceConfig = {
1011 ExecStart = "${cfg.package}/bin/puma -C config/puma.rb";
1012 Restart = "always";
1013 RestartSec = 20;
1014 EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ] ++ cfg.extraEnvFiles;
1015 WorkingDirectory = cfg.package;
1016 # Runtime directory and mode
1017 RuntimeDirectory = "mastodon-web";
1018 RuntimeDirectoryMode = "0750";
1019 # System Call Filtering
1020 SystemCallFilter = [
1021 ("~" + lib.concatStringsSep " " systemCallsList)
1022 "@chown"
1023 "pipe"
1024 "pipe2"
1025 ];
1026 }
1027 // cfgService;
1028 path = with pkgs; [
1029 ffmpeg-headless
1030 file
1031 ];
1032 };
1033
1034 systemd.services.mastodon-media-auto-remove = lib.mkIf cfg.mediaAutoRemove.enable {
1035 description = "Mastodon media auto remove";
1036 environment = env;
1037 serviceConfig = {
1038 Type = "oneshot";
1039 EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ] ++ cfg.extraEnvFiles;
1040 }
1041 // cfgService;
1042 script =
1043 let
1044 olderThanDays = toString cfg.mediaAutoRemove.olderThanDays;
1045 in
1046 ''
1047 ${cfg.package}/bin/tootctl media remove --days=${olderThanDays}
1048 ${cfg.package}/bin/tootctl preview_cards remove --days=${olderThanDays}
1049 '';
1050 startAt = cfg.mediaAutoRemove.startAt;
1051 };
1052
1053 services.nginx = lib.mkIf cfg.configureNginx {
1054 enable = true;
1055 recommendedProxySettings = true; # required for redirections to work
1056 virtualHosts."${cfg.localDomain}" = {
1057 root = "${cfg.package}/public/";
1058 # mastodon only supports https, but you can override this if you offload tls elsewhere.
1059 forceSSL = lib.mkDefault true;
1060 enableACME = lib.mkDefault true;
1061
1062 locations."/system/".alias = "/var/lib/mastodon/public-system/";
1063
1064 locations."/" = {
1065 tryFiles = "$uri @proxy";
1066 };
1067
1068 locations."@proxy" = {
1069 proxyPass = (
1070 if cfg.enableUnixSocket then
1071 "http://unix:/run/mastodon-web/web.socket"
1072 else
1073 "http://127.0.0.1:${toString cfg.webPort}"
1074 );
1075 proxyWebsockets = true;
1076 };
1077
1078 locations."/api/v1/streaming" = {
1079 proxyPass = "http://mastodon-streaming";
1080 proxyWebsockets = true;
1081 };
1082 };
1083 upstreams.mastodon-streaming = {
1084 extraConfig = ''
1085 least_conn;
1086 '';
1087 servers = builtins.listToAttrs (
1088 map (i: {
1089 name = "unix:/run/mastodon-streaming/streaming-${toString i}.socket";
1090 value = { };
1091 }) (lib.range 1 cfg.streamingProcesses)
1092 );
1093 };
1094 };
1095
1096 services.postfix = lib.mkIf (cfg.smtp.createLocally && cfg.smtp.host == "127.0.0.1") {
1097 enable = true;
1098 settings.main.myhostname = lib.mkDefault "${cfg.localDomain}";
1099 };
1100
1101 services.redis.servers.mastodon = lib.mkIf redisActuallyCreateLocally (
1102 lib.mkMerge [
1103 {
1104 enable = true;
1105 }
1106 (lib.mkIf (!cfg.redis.enableUnixSocket) {
1107 port = cfg.redis.port;
1108 })
1109 ]
1110 );
1111
1112 services.postgresql = lib.mkIf databaseActuallyCreateLocally {
1113 enable = true;
1114 ensureUsers = [
1115 {
1116 name = cfg.database.name;
1117 ensureDBOwnership = true;
1118 }
1119 ];
1120 ensureDatabases = [ cfg.database.name ];
1121 };
1122
1123 users.users = lib.mkMerge [
1124 (lib.mkIf (cfg.user == "mastodon") {
1125 mastodon = {
1126 isSystemUser = true;
1127 home = cfg.package;
1128 inherit (cfg) group;
1129 };
1130 })
1131 (lib.attrsets.setAttrByPath [ cfg.user "packages" ] [ cfg.package ])
1132 (lib.mkIf (cfg.redis.createLocally && cfg.redis.enableUnixSocket) {
1133 ${config.services.mastodon.user}.extraGroups = [ "redis-mastodon" ];
1134 })
1135 ];
1136
1137 users.groups.${cfg.group}.members = lib.optional cfg.configureNginx config.services.nginx.user;
1138 }
1139 {
1140 systemd.services = lib.mkMerge [
1141 sidekiqUnits
1142 streamingUnits
1143 ];
1144 }
1145 ]
1146 );
1147
1148 meta.maintainers = with lib.maintainers; [
1149 happy-river
1150 erictapen
1151 ];
1152
1153}