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 BOOTSNAP_CACHE_DIR="/var/cache/mastodon/precompile";
16 LD_PRELOAD = "${pkgs.jemalloc}/lib/libjemalloc.so";
18 MASTODON_USE_LIBVIPS = "true";
20 # Concurrency mastodon-web
21 WEB_CONCURRENCY = toString cfg.webProcesses;
22 MAX_THREADS = toString cfg.webThreads;
24 DB_USER = cfg.database.user;
26 DB_HOST = cfg.database.host;
27 DB_NAME = cfg.database.name;
28 LOCAL_DOMAIN = cfg.localDomain;
29 SMTP_SERVER = cfg.smtp.host;
30 SMTP_PORT = toString cfg.smtp.port;
31 SMTP_FROM_ADDRESS = cfg.smtp.fromAddress;
32 PAPERCLIP_ROOT_PATH = "/var/lib/mastodon/public-system";
33 PAPERCLIP_ROOT_URL = "/system";
34 ES_ENABLED = if (cfg.elasticsearch.host != null) then "true" else "false";
36 TRUSTED_PROXY_IP = cfg.trustedProxy;
38 // lib.optionalAttrs (cfg.redis.host != null) { REDIS_HOST = cfg.redis.host; }
39 // lib.optionalAttrs (cfg.redis.port != null) { REDIS_PORT = toString cfg.redis.port; }
40 // lib.optionalAttrs (cfg.redis.createLocally && cfg.redis.enableUnixSocket) { REDIS_URL = "unix://${config.services.redis.servers.mastodon.unixSocket}"; }
41 // lib.optionalAttrs (cfg.database.host != "/run/postgresql" && cfg.database.port != null) { DB_PORT = toString cfg.database.port; }
42 // lib.optionalAttrs cfg.smtp.authenticate { SMTP_LOGIN = cfg.smtp.user; }
43 // lib.optionalAttrs (cfg.elasticsearch.host != null) { ES_HOST = cfg.elasticsearch.host; }
44 // lib.optionalAttrs (cfg.elasticsearch.host != null) { ES_PORT = toString cfg.elasticsearch.port; }
45 // lib.optionalAttrs (cfg.elasticsearch.host != null) { ES_PRESET = cfg.elasticsearch.preset; }
46 // lib.optionalAttrs (cfg.elasticsearch.user != null) { ES_USER = cfg.elasticsearch.user; }
49 systemCallsList = [ "@cpu-emulation" "@debug" "@keyring" "@ipc" "@mount" "@obsolete" "@privileged" "@setuid" ];
56 WorkingDirectory = cfg.package;
57 # Cache directory and mode
58 CacheDirectory = "mastodon";
59 CacheDirectoryMode = "0750";
60 # State directory and mode
61 StateDirectory = "mastodon";
62 StateDirectoryMode = "0750";
63 # Logs directory and mode
64 LogsDirectory = "mastodon";
65 LogsDirectoryMode = "0750";
68 ProtectProc = "invisible";
69 # Access write directories
72 CapabilityBoundingSet = "";
74 NoNewPrivileges = true;
76 ProtectSystem = "strict";
79 PrivateDevices = true;
82 ProtectHostname = true;
83 ProtectKernelLogs = true;
84 ProtectKernelModules = true;
85 ProtectKernelTunables = true;
86 ProtectControlGroups = true;
87 RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" ];
88 RestrictNamespaces = true;
89 LockPersonality = true;
90 MemoryDenyWriteExecute = false;
91 RestrictRealtime = true;
92 RestrictSUIDSGID = true;
95 # System Call Filtering
96 SystemCallArchitectures = "native";
99 # Services that all Mastodon units After= and Requires= on
100 commonServices = lib.optional redisActuallyCreateLocally "redis-mastodon.service"
101 ++ lib.optional databaseActuallyCreateLocally "postgresql.service"
102 ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service";
104 envFile = pkgs.writeText "mastodon.env" (lib.concatMapStrings (s: s + "\n") (
105 (lib.concatLists (lib.mapAttrsToList (name: value:
106 lib.optional (value != null) ''${name}="${toString value}"''
109 mastodonTootctl = let
110 sourceExtraEnv = lib.concatMapStrings (p: "source ${p}\n") cfg.extraEnvFiles;
111 in pkgs.writeShellScriptBin "mastodon-tootctl" ''
113 export RAILS_ROOT="${cfg.package}"
115 source /var/lib/mastodon/.secrets_env
119 if [[ "$USER" != ${cfg.user} ]]; then
120 sudo='exec /run/wrappers/bin/sudo -u ${cfg.user} --preserve-env'
122 $sudo ${cfg.package}/bin/tootctl "$@"
125 sidekiqUnits = lib.attrsets.mapAttrs' (name: processCfg:
126 lib.nameValuePair "mastodon-sidekiq-${name}" (let
127 jobClassArgs = toString (builtins.map (c: "-q ${c}") processCfg.jobClasses);
128 jobClassLabel = toString ([""] ++ processCfg.jobClasses);
129 threads = toString (if processCfg.threads == null then cfg.sidekiqThreads else processCfg.threads);
131 after = [ "network.target" "mastodon-init-dirs.service" ] ++ commonServices;
132 requires = [ "mastodon-init-dirs.service" ] ++ commonServices;
133 description = "Mastodon sidekiq${jobClassLabel}";
134 wantedBy = [ "mastodon.target" ];
135 environment = env // {
136 PORT = toString cfg.sidekiqPort;
140 ExecStart = "${cfg.package}/bin/sidekiq ${jobClassArgs} -c ${threads} -r ${cfg.package}";
143 EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ] ++ cfg.extraEnvFiles;
144 WorkingDirectory = cfg.package;
145 LimitNOFILE = "1024000";
146 # System Call Filtering
147 SystemCallFilter = [ ("~" + lib.concatStringsSep " " systemCallsList) "@chown" "pipe" "pipe2" ];
149 path = with pkgs; [ ffmpeg-headless file imagemagick ];
151 ) cfg.sidekiqProcesses;
153 streamingUnits = builtins.listToAttrs
155 name = "mastodon-streaming-${toString i}";
157 after = [ "network.target" "mastodon-init-dirs.service" ] ++ commonServices;
158 requires = [ "mastodon-init-dirs.service" ] ++ commonServices;
159 wantedBy = [ "mastodon.target" "mastodon-streaming.target" ];
160 description = "Mastodon streaming ${toString i}";
161 environment = env // { SOCKET = "/run/mastodon-streaming/streaming-${toString i}.socket"; };
163 ExecStart = "${cfg.package}/run-streaming.sh";
166 EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ] ++ cfg.extraEnvFiles;
167 WorkingDirectory = cfg.package;
168 # Runtime directory and mode
169 RuntimeDirectory = "mastodon-streaming";
170 RuntimeDirectoryMode = "0750";
171 # System Call Filtering
172 SystemCallFilter = [ ("~" + lib.concatStringsSep " " (systemCallsList ++ [ "@memlock" "@resources" ])) "pipe" "pipe2" ];
176 (lib.range 1 cfg.streamingProcesses));
181 (lib.mkRemovedOptionModule
182 [ "services" "mastodon" "streamingPort" ]
183 "Mastodon currently doesn't support streaming via TCP ports. Please open a PR if you need this."
188 services.mastodon = {
189 enable = lib.mkEnableOption "Mastodon, a federated social network server";
191 configureNginx = lib.mkOption {
193 Configure nginx as a reverse proxy for mastodon.
194 Note that this makes some assumptions on your setup, and sets settings that will
195 affect other virtualHosts running on your nginx instance, if any.
196 Alternatively you can configure a reverse-proxy of your choice to serve these paths:
198 `/ -> $(nix-instantiate --eval '<nixpkgs>' -A mastodon.outPath)/public`
200 `/ -> 127.0.0.1:{{ webPort }} `(If there was no file in the directory above.)
202 `/system/ -> /var/lib/mastodon/public-system/`
204 `/api/v1/streaming/ -> 127.0.0.1:{{ streamingPort }}`
206 Make sure that websockets are forwarded properly. You might want to set up caching
207 of some requests. Take a look at mastodon's provided nginx configuration at
208 `https://github.com/mastodon/mastodon/blob/master/dist/nginx.conf`.
210 type = lib.types.bool;
214 user = lib.mkOption {
216 User under which mastodon runs. If it is set to "mastodon",
217 that user will be created, otherwise it should be set to the
218 name of a user created elsewhere.
219 In both cases, the `mastodon` package will be added to the user's package set
220 and a tootctl wrapper to system packages that switches to the configured account
221 and load the right environment.
223 type = lib.types.str;
224 default = "mastodon";
227 group = lib.mkOption {
229 Group under which mastodon runs.
231 type = lib.types.str;
232 default = "mastodon";
235 streamingProcesses = lib.mkOption {
237 Number of processes used by the mastodon-streaming service.
238 Please define this explicitly, recommended is the amount of your CPU cores minus one.
240 type = lib.types.ints.positive;
244 webPort = lib.mkOption {
245 description = "TCP port used by the mastodon-web service.";
246 type = lib.types.port;
249 webProcesses = lib.mkOption {
250 description = "Processes used by the mastodon-web service.";
251 type = lib.types.int;
254 webThreads = lib.mkOption {
255 description = "Threads per process used by the mastodon-web service.";
256 type = lib.types.int;
260 sidekiqPort = lib.mkOption {
261 description = "TCP port used by the mastodon-sidekiq service.";
262 type = lib.types.port;
266 sidekiqThreads = lib.mkOption {
267 description = "Worker threads used by the mastodon-sidekiq-all service. If `sidekiqProcesses` is configured and any processes specify null `threads`, this value is used.";
268 type = lib.types.int;
272 sidekiqProcesses = lib.mkOption {
273 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!*";
274 type = with lib.types; attrsOf (submodule {
276 jobClasses = lib.mkOption {
277 type = listOf (enum [ "default" "push" "pull" "mailers" "scheduler" "ingress" ]);
278 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.*";
280 threads = lib.mkOption {
282 description = "Number of threads this process should use for executing jobs. If null, the configured `sidekiqThreads` are used.";
298 jobClasses = [ "ingress" ];
302 jobClasses = [ "default" ];
306 jobClasses = [ "push" "pull" ];
312 vapidPublicKeyFile = lib.mkOption {
314 Path to file containing the public key used for Web Push
315 Voluntary Application Server Identification. A new keypair can
316 be generated by running:
318 `nix build -f '<nixpkgs>' mastodon; cd result; RAILS_ENV=production bin/rake webpush:generate_keys`
320 If {option}`mastodon.vapidPrivateKeyFile`does not
321 exist, it and this file will be created with a new keypair.
323 default = "/var/lib/mastodon/secrets/vapid-public-key";
324 type = lib.types.str;
327 localDomain = lib.mkOption {
328 description = "The domain serving your Mastodon instance.";
329 example = "social.example.org";
330 type = lib.types.str;
333 activeRecordEncryptionDeterministicKeyFile = lib.mkOption {
335 This key must be set to enable the Active Record Encryption feature within
336 Rails that Mastodon uses to encrypt and decrypt some database attributes.
337 A new Active Record keys can be generated by running:
339 `nix build -f '<nixpkgs>' mastodon; cd result; RAILS_ENV=production ./bin/rails db:encryption:init`
341 If this file does not exist, it will be created with a new Active Record
344 default = "/var/lib/mastodon/secrets/active-record-encryption-deterministic-key";
345 type = lib.types.str;
348 activeRecordEncryptionKeyDerivationSaltFile = lib.mkOption {
350 This key must be set to enable the Active Record Encryption feature within
351 Rails that Mastodon uses to encrypt and decrypt some database attributes.
352 A new Active Record keys can be generated by running:
354 `nix build -f '<nixpkgs>' mastodon; cd result; RAILS_ENV=production ./bin/rails db:encryption:init`
356 If this file does not exist, it will be created with a new Active Record
359 default = "/var/lib/mastodon/secrets/active-record-encryption-key-derivation-salt";
360 type = lib.types.str;
363 activeRecordEncryptionPrimaryKeyFile = lib.mkOption {
365 This key must be set to enable the Active Record Encryption feature within
366 Rails that Mastodon uses to encrypt and decrypt some database attributes.
367 A new Active Record keys can be generated by running:
369 `nix build -f '<nixpkgs>' mastodon; cd result; RAILS_ENV=production ./bin/rails db:encryption:init`
371 If this file does not exist, it will be created with a new Active Record
374 default = "/var/lib/mastodon/secrets/active-record-encryption-primary-key";
375 type = lib.types.str;
378 secretKeyBaseFile = lib.mkOption {
380 Path to file containing the secret key base.
381 A new secret key base can be generated by running:
383 `nix build -f '<nixpkgs>' mastodon; cd result; bin/bundle exec rails secret`
385 If this file does not exist, it will be created with a new secret key base.
387 default = "/var/lib/mastodon/secrets/secret-key-base";
388 type = lib.types.str;
391 otpSecretFile = lib.mkOption {
393 Path to file containing the OTP secret.
394 A new OTP secret can be generated by running:
396 `nix build -f '<nixpkgs>' mastodon; cd result; bin/bundle exec rails secret`
398 If this file does not exist, it will be created with a new OTP secret.
400 default = "/var/lib/mastodon/secrets/otp-secret";
401 type = lib.types.str;
404 vapidPrivateKeyFile = lib.mkOption {
406 Path to file containing the private key used for Web Push
407 Voluntary Application Server Identification. A new keypair can
408 be generated by running:
410 `nix build -f '<nixpkgs>' mastodon; cd result; bin/rake webpush:generate_keys`
412 If this file does not exist, it will be created with a new
415 default = "/var/lib/mastodon/secrets/vapid-private-key";
416 type = lib.types.str;
419 trustedProxy = lib.mkOption {
421 You need to set it to the IP from which your reverse proxy sends requests to Mastodon's web process,
422 otherwise Mastodon will record the reverse proxy's own IP as the IP of all requests, which would be
423 bad because IP addresses are used for important rate limits and security functions.
425 type = lib.types.str;
426 default = "127.0.0.1";
429 enableUnixSocket = lib.mkOption {
431 Instead of binding to an IP address like 127.0.0.1, you may bind to a Unix socket. This variable
432 is process-specific, e.g. you need different values for every process, and it works for both web (Puma)
433 processes and streaming API (Node.js) processes.
435 type = lib.types.bool;
440 createLocally = lib.mkOption {
441 description = "Configure local Redis server for Mastodon.";
442 type = lib.types.bool;
446 host = lib.mkOption {
447 description = "Redis host.";
448 type = lib.types.nullOr lib.types.str;
449 default = if cfg.redis.createLocally && !cfg.redis.enableUnixSocket then "127.0.0.1" else null;
450 defaultText = lib.literalExpression ''
451 if config.${opt.redis.createLocally} && !config.${opt.redis.enableUnixSocket} then "127.0.0.1" else null
455 port = lib.mkOption {
456 description = "Redis port.";
457 type = lib.types.nullOr lib.types.port;
458 default = if cfg.redis.createLocally && !cfg.redis.enableUnixSocket then 31637 else null;
459 defaultText = lib.literalExpression ''
460 if config.${opt.redis.createLocally} && !config.${opt.redis.enableUnixSocket} then 31637 else null
464 passwordFile = lib.mkOption {
465 description = "A file containing the password for Redis database.";
466 type = lib.types.nullOr lib.types.path;
468 example = "/run/keys/mastodon-redis-password";
471 enableUnixSocket = lib.mkOption {
472 description = "Use Unix socket";
473 type = lib.types.bool;
479 createLocally = lib.mkOption {
480 description = "Configure local PostgreSQL database server for Mastodon.";
481 type = lib.types.bool;
485 host = lib.mkOption {
486 type = lib.types.str;
487 default = "/run/postgresql";
488 example = "192.168.23.42";
489 description = "Database host address or unix socket.";
492 port = lib.mkOption {
493 type = lib.types.nullOr lib.types.port;
494 default = if cfg.database.createLocally then null else 5432;
495 defaultText = lib.literalExpression ''
496 if config.${opt.database.createLocally}
500 description = "Database host port.";
503 name = lib.mkOption {
504 type = lib.types.str;
505 default = "mastodon";
506 description = "Database name.";
509 user = lib.mkOption {
510 type = lib.types.str;
511 default = "mastodon";
512 description = "Database user.";
515 passwordFile = lib.mkOption {
516 type = lib.types.nullOr lib.types.path;
518 example = "/var/lib/mastodon/secrets/db-password";
520 A file containing the password corresponding to
521 {option}`database.user`.
527 createLocally = lib.mkOption {
528 description = "Configure local Postfix SMTP server for Mastodon.";
529 type = lib.types.bool;
533 authenticate = lib.mkOption {
534 description = "Authenticate with the SMTP server using username and password.";
535 type = lib.types.bool;
539 host = lib.mkOption {
540 description = "SMTP host used when sending emails to users.";
541 type = lib.types.str;
542 default = "127.0.0.1";
545 port = lib.mkOption {
546 description = "SMTP port used when sending emails to users.";
547 type = lib.types.port;
551 fromAddress = lib.mkOption {
552 description = ''"From" address used when sending Emails to users.'';
553 type = lib.types.str;
556 user = lib.mkOption {
557 type = lib.types.nullOr lib.types.str;
559 example = "mastodon@example.com";
560 description = "SMTP login name.";
563 passwordFile = lib.mkOption {
564 type = lib.types.nullOr lib.types.path;
566 example = "/var/lib/mastodon/secrets/smtp-password";
568 Path to file containing the SMTP password.
574 host = lib.mkOption {
577 If it is not null, Elasticsearch full text search will be enabled.
579 type = lib.types.nullOr lib.types.str;
583 port = lib.mkOption {
584 description = "Elasticsearch port.";
585 type = lib.types.port;
589 preset = lib.mkOption {
591 It controls the ElasticSearch indices configuration (number of shards and replica).
593 type = lib.types.enum [ "single_node_cluster" "small_cluster" "large_cluster" ];
594 default = "single_node_cluster";
595 example = "large_cluster";
598 user = lib.mkOption {
599 description = "Used for optionally authenticating with Elasticsearch.";
600 type = lib.types.nullOr lib.types.str;
602 example = "elasticsearch-mastodon";
605 passwordFile = lib.mkOption {
607 Path to file containing password for optionally authenticating with Elasticsearch.
609 type = lib.types.nullOr lib.types.path;
611 example = "/var/lib/mastodon/secrets/elasticsearch-password";
615 package = lib.mkOption {
616 type = lib.types.package;
617 default = pkgs.mastodon;
618 defaultText = lib.literalExpression "pkgs.mastodon";
619 description = "Mastodon package to use.";
622 extraConfig = lib.mkOption {
623 type = lib.types.attrs;
626 Extra environment variables to pass to all mastodon services.
630 extraEnvFiles = lib.mkOption {
631 type = with lib.types; listOf path;
634 Extra environment files to pass to all mastodon services. Useful for passing down environmental secrets.
636 example = [ "/etc/mastodon/s3config.env" ];
639 automaticMigrations = lib.mkOption {
640 type = lib.types.bool;
643 Do automatic database migrations.
648 enable = lib.mkOption {
649 type = lib.types.bool;
653 Automatically remove remote media attachments and preview cards older than the configured amount of days.
655 Recommended in https://docs.joinmastodon.org/admin/setup/.
659 startAt = lib.mkOption {
660 type = lib.types.str;
664 How often to remove remote media.
666 The format is described in {manpage}`systemd.time(7)`.
670 olderThanDays = lib.mkOption {
671 type = lib.types.int;
675 How old remote media needs to be in order to be removed.
682 config = lib.mkIf cfg.enable (lib.mkMerge [{
685 assertion = !redisActuallyCreateLocally -> (cfg.redis.host != "127.0.0.1" && cfg.redis.port != null);
687 `services.mastodon.redis.host` and `services.mastodon.redis.port` need to be set if
688 `services.mastodon.redis.createLocally` is not enabled.
692 assertion = redisActuallyCreateLocally -> (!cfg.redis.enableUnixSocket || (cfg.redis.host == null && cfg.redis.port == null));
694 `services.mastodon.redis.enableUnixSocket` needs to be disabled if
695 `services.mastodon.redis.host` and `services.mastodon.redis.port` is used.
699 assertion = redisActuallyCreateLocally -> (!cfg.redis.enableUnixSocket || cfg.redis.passwordFile == null);
701 <option>services.mastodon.redis.enableUnixSocket</option> needs to be disabled if
702 <option>services.mastodon.redis.passwordFile</option> is used.
706 assertion = databaseActuallyCreateLocally -> (cfg.user == cfg.database.user && cfg.database.user == cfg.database.name);
708 For local automatic database provisioning (services.mastodon.database.createLocally == true) with peer
709 authentication (services.mastodon.database.host == "/run/postgresql") to work services.mastodon.user
710 and services.mastodon.database.user must be identical.
714 assertion = !databaseActuallyCreateLocally -> (cfg.database.host != "/run/postgresql");
716 <option>services.mastodon.database.host</option> needs to be set if
717 <option>services.mastodon.database.createLocally</option> is not enabled.
721 assertion = cfg.smtp.authenticate -> (cfg.smtp.user != null);
723 <option>services.mastodon.smtp.user</option> needs to be set if
724 <option>services.mastodon.smtp.authenticate</option> is enabled.
728 assertion = cfg.smtp.authenticate -> (cfg.smtp.passwordFile != null);
730 <option>services.mastodon.smtp.passwordFile</option> needs to be set if
731 <option>services.mastodon.smtp.authenticate</option> is enabled.
738 (_: v: builtins.elem "scheduler" v.jobClasses || v.jobClasses == [ ])
739 cfg.sidekiqProcesses));
740 message = "There must be exactly one Sidekiq queue in services.mastodon.sidekiqProcesses with jobClass \"scheduler\".";
744 environment.systemPackages = [ mastodonTootctl ];
746 systemd.targets.mastodon = {
747 description = "Target for all Mastodon services";
748 wantedBy = [ "multi-user.target" ];
749 after = [ "network.target" ];
752 systemd.targets.mastodon-streaming = {
753 description = "Target for all Mastodon streaming services";
754 wantedBy = [ "multi-user.target" "mastodon.target" ];
755 after = [ "network.target" ];
758 systemd.services.mastodon-init-dirs = {
762 if ! test -d /var/cache/mastodon/precompile; then
763 ${cfg.package}/bin/bundle exec bootsnap precompile --gemfile ${cfg.package}/app ${cfg.package}/lib
765 if ! test -f ${cfg.activeRecordEncryptionDeterministicKeyFile}; then
766 mkdir -p $(dirname ${cfg.activeRecordEncryptionDeterministicKeyFile})
767 bin/rails db:encryption:init | grep --only-matching "ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=[^ ]\+" | sed 's/^ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=//' > ${cfg.activeRecordEncryptionDeterministicKeyFile}
769 if ! test -f ${cfg.activeRecordEncryptionKeyDerivationSaltFile}; then
770 mkdir -p $(dirname ${cfg.activeRecordEncryptionKeyDerivationSaltFile})
771 bin/rails db:encryption:init | grep --only-matching "ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=[^ ]\+" | sed 's/^ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=//' > ${cfg.activeRecordEncryptionKeyDerivationSaltFile}
773 if ! test -f ${cfg.activeRecordEncryptionPrimaryKeyFile}; then
774 mkdir -p $(dirname ${cfg.activeRecordEncryptionPrimaryKeyFile})
775 bin/rails db:encryption:init | grep --only-matching "ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=[^ ]\+" | sed 's/^ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=//' > ${cfg.activeRecordEncryptionPrimaryKeyFile}
777 if ! test -f ${cfg.secretKeyBaseFile}; then
778 mkdir -p $(dirname ${cfg.secretKeyBaseFile})
779 bin/bundle exec rails secret > ${cfg.secretKeyBaseFile}
781 if ! test -f ${cfg.otpSecretFile}; then
782 mkdir -p $(dirname ${cfg.otpSecretFile})
783 bin/bundle exec rails secret > ${cfg.otpSecretFile}
785 if ! test -f ${cfg.vapidPrivateKeyFile}; then
786 mkdir -p $(dirname ${cfg.vapidPrivateKeyFile}) $(dirname ${cfg.vapidPublicKeyFile})
787 keypair=$(bin/rake webpush:generate_keys)
788 echo $keypair | grep --only-matching "Private -> [^ ]\+" | sed 's/^Private -> //' > ${cfg.vapidPrivateKeyFile}
789 echo $keypair | grep --only-matching "Public -> [^ ]\+" | sed 's/^Public -> //' > ${cfg.vapidPublicKeyFile}
792 cat > /var/lib/mastodon/.secrets_env <<EOF
793 ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY="$(cat ${cfg.activeRecordEncryptionDeterministicKeyFile})"
794 ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT="$(cat ${cfg.activeRecordEncryptionKeyDerivationSaltFile})"
795 ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY="$(cat ${cfg.activeRecordEncryptionPrimaryKeyFile})"
796 SECRET_KEY_BASE="$(cat ${cfg.secretKeyBaseFile})"
797 OTP_SECRET="$(cat ${cfg.otpSecretFile})"
798 VAPID_PRIVATE_KEY="$(cat ${cfg.vapidPrivateKeyFile})"
799 VAPID_PUBLIC_KEY="$(cat ${cfg.vapidPublicKeyFile})"
800 '' + lib.optionalString (cfg.redis.passwordFile != null)''
801 REDIS_PASSWORD="$(cat ${cfg.redis.passwordFile})"
802 '' + lib.optionalString (cfg.database.passwordFile != null) ''
803 DB_PASS="$(cat ${cfg.database.passwordFile})"
804 '' + lib.optionalString cfg.smtp.authenticate ''
805 SMTP_PASSWORD="$(cat ${cfg.smtp.passwordFile})"
806 '' + lib.optionalString (cfg.elasticsearch.passwordFile != null) ''
807 ES_PASS="$(cat ${cfg.elasticsearch.passwordFile})"
814 SyslogIdentifier = "mastodon-init-dirs";
815 # System Call Filtering
816 SystemCallFilter = [ ("~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ])) "@chown" "pipe" "pipe2" ];
819 after = [ "network.target" ];
822 systemd.services.mastodon-init-db = lib.mkIf cfg.automaticMigrations {
823 script = lib.optionalString (!databaseActuallyCreateLocally) ''
825 export PGPASSWORD="$(cat '${cfg.database.passwordFile}')"
827 result="$(psql -t --csv -c \
828 "select count(*) from pg_class c \
829 join pg_namespace s on s.oid = c.relnamespace \
830 where s.nspname not in ('pg_catalog', 'pg_toast', 'information_schema') \
831 and s.nspname not like 'pg_temp%';")" || error_code=$?
832 if [ "''${error_code:-0}" -ne 0 ]; then
833 echo "Failure checking if database is seeded. psql gave exit code $error_code"
836 if [ "$result" -eq 0 ]; then
837 echo "Seeding database"
838 SAFETY_ASSURED=1 rails db:schema:load
841 echo "Migrating database (this might be a noop)"
844 '' + lib.optionalString (!databaseActuallyCreateLocally) ''
847 path = [ cfg.package config.services.postgresql.package ];
848 environment = env // lib.optionalAttrs (!databaseActuallyCreateLocally) {
849 PGHOST = cfg.database.host;
850 PGPORT = toString cfg.database.port;
851 PGDATABASE = cfg.database.name;
852 PGUSER = cfg.database.user;
856 EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ] ++ cfg.extraEnvFiles;
857 WorkingDirectory = cfg.package;
858 # System Call Filtering
859 SystemCallFilter = [ ("~" + lib.concatStringsSep " " (systemCallsList ++ [ "@resources" ])) "@chown" "pipe" "pipe2" ];
861 after = [ "network.target" "mastodon-init-dirs.service" ]
862 ++ lib.optional databaseActuallyCreateLocally "postgresql.service";
863 requires = [ "mastodon-init-dirs.service" ]
864 ++ lib.optional databaseActuallyCreateLocally "postgresql.service";
867 systemd.services.mastodon-web = {
868 after = [ "network.target" "mastodon-init-dirs.service" ] ++ commonServices;
869 requires = [ "mastodon-init-dirs.service" ] ++ commonServices;
870 wantedBy = [ "mastodon.target" ];
871 description = "Mastodon web";
872 environment = env // (if cfg.enableUnixSocket
873 then { SOCKET = "/run/mastodon-web/web.socket"; }
874 else { PORT = toString cfg.webPort; }
877 ExecStart = "${cfg.package}/bin/puma -C config/puma.rb";
880 EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ] ++ cfg.extraEnvFiles;
881 WorkingDirectory = cfg.package;
882 # Runtime directory and mode
883 RuntimeDirectory = "mastodon-web";
884 RuntimeDirectoryMode = "0750";
885 # System Call Filtering
886 SystemCallFilter = [ ("~" + lib.concatStringsSep " " systemCallsList) "@chown" "pipe" "pipe2" ];
888 path = with pkgs; [ ffmpeg-headless file ];
891 systemd.services.mastodon-media-auto-remove = lib.mkIf cfg.mediaAutoRemove.enable {
892 description = "Mastodon media auto remove";
896 EnvironmentFile = [ "/var/lib/mastodon/.secrets_env" ] ++ cfg.extraEnvFiles;
899 olderThanDays = toString cfg.mediaAutoRemove.olderThanDays;
901 ${cfg.package}/bin/tootctl media remove --days=${olderThanDays}
902 ${cfg.package}/bin/tootctl preview_cards remove --days=${olderThanDays}
904 startAt = cfg.mediaAutoRemove.startAt;
907 services.nginx = lib.mkIf cfg.configureNginx {
909 recommendedProxySettings = true; # required for redirections to work
910 virtualHosts."${cfg.localDomain}" = {
911 root = "${cfg.package}/public/";
912 # mastodon only supports https, but you can override this if you offload tls elsewhere.
913 forceSSL = lib.mkDefault true;
914 enableACME = lib.mkDefault true;
916 locations."/system/".alias = "/var/lib/mastodon/public-system/";
919 tryFiles = "$uri @proxy";
922 locations."@proxy" = {
923 proxyPass = (if cfg.enableUnixSocket then "http://unix:/run/mastodon-web/web.socket" else "http://127.0.0.1:${toString cfg.webPort}");
924 proxyWebsockets = true;
927 locations."/api/v1/streaming/" = {
928 proxyPass = "http://mastodon-streaming";
929 proxyWebsockets = true;
932 upstreams.mastodon-streaming = {
936 servers = builtins.listToAttrs
938 name = "unix:/run/mastodon-streaming/streaming-${toString i}.socket";
940 }) (lib.range 1 cfg.streamingProcesses));
944 services.postfix = lib.mkIf (cfg.smtp.createLocally && cfg.smtp.host == "127.0.0.1") {
946 hostname = lib.mkDefault "${cfg.localDomain}";
948 services.redis.servers.mastodon = lib.mkIf redisActuallyCreateLocally (lib.mkMerge [
952 (lib.mkIf (!cfg.redis.enableUnixSocket) {
953 port = cfg.redis.port;
956 services.postgresql = lib.mkIf databaseActuallyCreateLocally {
960 name = cfg.database.name;
961 ensureDBOwnership = true;
964 ensureDatabases = [ cfg.database.name ];
967 users.users = lib.mkMerge [
968 (lib.mkIf (cfg.user == "mastodon") {
975 (lib.attrsets.setAttrByPath [ cfg.user "packages" ] [ cfg.package ])
976 (lib.mkIf (cfg.redis.createLocally && cfg.redis.enableUnixSocket) {${config.services.mastodon.user}.extraGroups = [ "redis-mastodon" ];})
979 users.groups.${cfg.group}.members = lib.optional cfg.configureNginx config.services.nginx.user;
981 { systemd.services = lib.mkMerge [ sidekiqUnits streamingUnits ]; }
984 meta.maintainers = with lib.maintainers; [ happy-river erictapen ];