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