1 { lib, pkgs, config, options, ... }:
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";
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;
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; }
46 systemCallsList = [ "@cpu-emulation" "@debug" "@keyring" "@ipc" "@mount" "@obsolete" "@privileged" "@setuid" ];
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";
62 ProtectProc = "invisible";
63 # Access write directories
66 CapabilityBoundingSet = "";
68 NoNewPrivileges = true;
70 ProtectSystem = "strict";
73 PrivateDevices = 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;
89 # System Call Filtering
90 SystemCallArchitectures = "native";
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}"''
103 mastodonTootctl = let
104 sourceExtraEnv = lib.concatMapStrings (p: "source ${p}\n") cfg.extraEnvFiles;
105 in pkgs.writeShellScriptBin "mastodon-tootctl" ''
107 export RAILS_ROOT="${cfg.package}"
109 source /var/lib/mastodon/.secrets_env
113 if [[ "$USER" != ${cfg.user} ]]; then
114 sudo='exec /run/wrappers/bin/sudo -u ${cfg.user} --preserve-env'
116 $sudo ${cfg.package}/bin/tootctl "$@"
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);
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);
134 ExecStart = "${cfg.package}/bin/sidekiq ${jobClassArgs} -c ${threads} -r ${cfg.package}";
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" ];
143 path = with pkgs; [ ffmpeg-headless file imagemagick ];
145 ) cfg.sidekiqProcesses;
147 streamingUnits = builtins.listToAttrs
149 name = "mastodon-streaming-${toString i}";
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"; };
157 ExecStart = "${cfg.package}/run-streaming.sh";
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" ];
170 (lib.range 1 cfg.streamingProcesses));
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."
182 services.mastodon = {
183 enable = lib.mkEnableOption "Mastodon, a federated social network server";
185 configureNginx = lib.mkOption {
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`.
204 type = lib.types.bool;
208 user = lib.mkOption {
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.
217 type = lib.types.str;
218 default = "mastodon";
221 group = lib.mkOption {
223 Group under which mastodon runs.
225 type = lib.types.str;
226 default = "mastodon";
229 streamingProcesses = lib.mkOption {
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.
234 type = lib.types.ints.positive;
238 webPort = lib.mkOption {
239 description = "TCP port used by the mastodon-web service.";
240 type = lib.types.port;
243 webProcesses = lib.mkOption {
244 description = "Processes used by the mastodon-web service.";
245 type = lib.types.int;
248 webThreads = lib.mkOption {
249 description = "Threads per process used by the mastodon-web service.";
250 type = lib.types.int;
254 sidekiqPort = lib.mkOption {
255 description = "TCP port used by the mastodon-sidekiq service.";
256 type = lib.types.port;
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;
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 {
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.*";
274 threads = lib.mkOption {
276 description = "Number of threads this process should use for executing jobs. If null, the configured `sidekiqThreads` are used.";
292 jobClasses = [ "ingress" ];
296 jobClasses = [ "default" ];
300 jobClasses = [ "push" "pull" ];
306 vapidPublicKeyFile = lib.mkOption {
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.
317 default = "/var/lib/mastodon/secrets/vapid-public-key";
318 type = lib.types.str;
321 localDomain = lib.mkOption {
322 description = "The domain serving your Mastodon instance.";
323 example = "social.example.org";
324 type = lib.types.str;
327 secretKeyBaseFile = lib.mkOption {
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.
336 default = "/var/lib/mastodon/secrets/secret-key-base";
337 type = lib.types.str;
340 otpSecretFile = lib.mkOption {
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.
349 default = "/var/lib/mastodon/secrets/otp-secret";
350 type = lib.types.str;
353 vapidPrivateKeyFile = lib.mkOption {
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
364 default = "/var/lib/mastodon/secrets/vapid-private-key";
365 type = lib.types.str;
368 trustedProxy = lib.mkOption {
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.
374 type = lib.types.str;
375 default = "127.0.0.1";
378 enableUnixSocket = lib.mkOption {
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.
384 type = lib.types.bool;
389 createLocally = lib.mkOption {
390 description = "Configure local Redis server for Mastodon.";
391 type = lib.types.bool;
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
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
413 passwordFile = lib.mkOption {
414 description = "A file containing the password for Redis database.";
415 type = lib.types.nullOr lib.types.path;
417 example = "/run/keys/mastodon-redis-password";
420 enableUnixSocket = lib.mkOption {
421 description = "Use Unix socket";
422 type = lib.types.bool;
428 createLocally = lib.mkOption {
429 description = "Configure local PostgreSQL database server for Mastodon.";
430 type = lib.types.bool;
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.";
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}
449 description = "Database host port.";
452 name = lib.mkOption {
453 type = lib.types.str;
454 default = "mastodon";
455 description = "Database name.";
458 user = lib.mkOption {
459 type = lib.types.str;
460 default = "mastodon";
461 description = "Database user.";
464 passwordFile = lib.mkOption {
465 type = lib.types.nullOr lib.types.path;
467 example = "/var/lib/mastodon/secrets/db-password";
469 A file containing the password corresponding to
470 {option}`database.user`.
476 createLocally = lib.mkOption {
477 description = "Configure local Postfix SMTP server for Mastodon.";
478 type = lib.types.bool;
482 authenticate = lib.mkOption {
483 description = "Authenticate with the SMTP server using username and password.";
484 type = lib.types.bool;
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";
494 port = lib.mkOption {
495 description = "SMTP port used when sending emails to users.";
496 type = lib.types.port;
500 fromAddress = lib.mkOption {
501 description = ''"From" address used when sending Emails to users.'';
502 type = lib.types.str;
505 user = lib.mkOption {
506 type = lib.types.nullOr lib.types.str;
508 example = "mastodon@example.com";
509 description = "SMTP login name.";
512 passwordFile = lib.mkOption {
513 type = lib.types.nullOr lib.types.path;
515 example = "/var/lib/mastodon/secrets/smtp-password";
517 Path to file containing the SMTP password.
523 host = lib.mkOption {
526 If it is not null, Elasticsearch full text search will be enabled.
528 type = lib.types.nullOr lib.types.str;
532 port = lib.mkOption {
533 description = "Elasticsearch port.";
534 type = lib.types.port;
538 preset = lib.mkOption {
540 It controls the ElasticSearch indices configuration (number of shards and replica).
542 type = lib.types.enum [ "single_node_cluster" "small_cluster" "large_cluster" ];
543 default = "single_node_cluster";
544 example = "large_cluster";
547 user = lib.mkOption {
548 description = "Used for optionally authenticating with Elasticsearch.";
549 type = lib.types.nullOr lib.types.str;
551 example = "elasticsearch-mastodon";
554 passwordFile = lib.mkOption {
556 Path to file containing password for optionally authenticating with Elasticsearch.
558 type = lib.types.nullOr lib.types.path;
560 example = "/var/lib/mastodon/secrets/elasticsearch-password";
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.";
571 extraConfig = lib.mkOption {
572 type = lib.types.attrs;
575 Extra environment variables to pass to all mastodon services.
579 extraEnvFiles = lib.mkOption {
580 type = with lib.types; listOf path;
583 Extra environment files to pass to all mastodon services. Useful for passing down environmental secrets.
585 example = [ "/etc/mastodon/s3config.env" ];
588 automaticMigrations = lib.mkOption {
589 type = lib.types.bool;
592 Do automatic database migrations.
597 enable = lib.mkOption {
598 type = lib.types.bool;
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/.
608 startAt = lib.mkOption {
609 type = lib.types.str;
613 How often to remove remote media.
615 The format is described in {manpage}`systemd.time(7)`.
619 olderThanDays = lib.mkOption {
620 type = lib.types.int;
624 How old remote media needs to be in order to be removed.
631 config = lib.mkIf cfg.enable (lib.mkMerge [{
634 assertion = !redisActuallyCreateLocally -> (cfg.redis.host != "127.0.0.1" && cfg.redis.port != null);
636 `services.mastodon.redis.host` and `services.mastodon.redis.port` need to be set if
637 `services.mastodon.redis.createLocally` is not enabled.
641 assertion = redisActuallyCreateLocally -> (!cfg.redis.enableUnixSocket || (cfg.redis.host == null && cfg.redis.port == null));
643 `services.mastodon.redis.enableUnixSocket` needs to be disabled if
644 `services.mastodon.redis.host` and `services.mastodon.redis.port` is used.
648 assertion = redisActuallyCreateLocally -> (!cfg.redis.enableUnixSocket || cfg.redis.passwordFile == null);
650 <option>services.mastodon.redis.enableUnixSocket</option> needs to be disabled if
651 <option>services.mastodon.redis.passwordFile</option> is used.
655 assertion = databaseActuallyCreateLocally -> (cfg.user == cfg.database.user && cfg.database.user == cfg.database.name);
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.
663 assertion = !databaseActuallyCreateLocally -> (cfg.database.host != "/run/postgresql");
665 <option>services.mastodon.database.host</option> needs to be set if
666 <option>services.mastodon.database.createLocally</option> is not enabled.
670 assertion = cfg.smtp.authenticate -> (cfg.smtp.user != null);
672 <option>services.mastodon.smtp.user</option> needs to be set if
673 <option>services.mastodon.smtp.authenticate</option> is enabled.
677 assertion = cfg.smtp.authenticate -> (cfg.smtp.passwordFile != null);
679 <option>services.mastodon.smtp.passwordFile</option> needs to be set if
680 <option>services.mastodon.smtp.authenticate</option> is enabled.
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\".";
693 environment.systemPackages = [ mastodonTootctl ];
695 systemd.targets.mastodon = {
696 description = "Target for all Mastodon services";
697 wantedBy = [ "multi-user.target" ];
698 after = [ "network.target" ];
701 systemd.targets.mastodon-streaming = {
702 description = "Target for all Mastodon streaming services";
703 wantedBy = [ "multi-user.target" "mastodon.target" ];
704 after = [ "network.target" ];
707 systemd.services.mastodon-init-dirs = {
711 if ! test -f ${cfg.secretKeyBaseFile}; then
712 mkdir -p $(dirname ${cfg.secretKeyBaseFile})
713 bin/rake secret > ${cfg.secretKeyBaseFile}
715 if ! test -f ${cfg.otpSecretFile}; then
716 mkdir -p $(dirname ${cfg.otpSecretFile})
717 bin/rake secret > ${cfg.otpSecretFile}
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}
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})"
745 SyslogIdentifier = "mastodon-init-dirs";
746 # System Call Filtering
747 SystemCallFilter = [ ("~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ])) "@chown" "pipe" "pipe2" ];
750 after = [ "network.target" ];
753 systemd.services.mastodon-init-db = lib.mkIf cfg.automaticMigrations {
754 script = lib.optionalString (!databaseActuallyCreateLocally) ''
756 export PGPASSWORD="$(cat '${cfg.database.passwordFile}')"
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"
767 if [ "$result" -eq 0 ]; then
768 echo "Seeding database"
769 SAFETY_ASSURED=1 rails db:schema:load
772 echo "Migrating database (this might be a noop)"
775 '' + lib.optionalString (!databaseActuallyCreateLocally) ''
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;
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" ];
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";
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); }
808 ExecStart = "${cfg.package}/bin/puma -C config/puma.rb";
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" ];
819 path = with pkgs; [ ffmpeg-headless file imagemagick ];
822 systemd.services.mastodon-media-auto-remove = lib.mkIf cfg.mediaAutoRemove.enable {
823 description = "Mastodon media auto remove";
827 EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ] ++ cfg.extraEnvFiles;
830 olderThanDays = toString cfg.mediaAutoRemove.olderThanDays;
832 ${cfg.package}/bin/tootctl media remove --days=${olderThanDays}
833 ${cfg.package}/bin/tootctl preview_cards remove --days=${olderThanDays}
835 startAt = cfg.mediaAutoRemove.startAt;
838 services.nginx = lib.mkIf cfg.configureNginx {
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/";
850 tryFiles = "$uri @proxy";
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;
858 locations."/api/v1/streaming/" = {
859 proxyPass = "http://mastodon-streaming";
860 proxyWebsockets = true;
863 upstreams.mastodon-streaming = {
867 servers = builtins.listToAttrs
869 name = "unix:/run/mastodon-streaming/streaming-${toString i}.socket";
871 }) (lib.range 1 cfg.streamingProcesses));
875 services.postfix = lib.mkIf (cfg.smtp.createLocally && cfg.smtp.host == "127.0.0.1") {
877 hostname = lib.mkDefault "${cfg.localDomain}";
879 services.redis.servers.mastodon = lib.mkIf redisActuallyCreateLocally (lib.mkMerge [
883 (lib.mkIf (!cfg.redis.enableUnixSocket) {
884 port = cfg.redis.port;
887 services.postgresql = lib.mkIf databaseActuallyCreateLocally {
891 name = cfg.database.name;
892 ensureDBOwnership = true;
895 ensureDatabases = [ cfg.database.name ];
898 users.users = lib.mkMerge [
899 (lib.mkIf (cfg.user == "mastodon") {
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" ];})
910 users.groups.${cfg.group}.members = lib.optional cfg.configureNginx config.services.nginx.user;
912 { systemd.services = lib.mkMerge [ sidekiqUnits streamingUnits ]; }
915 meta.maintainers = with lib.maintainers; [ happy-river erictapen ];