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