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