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