{ description = "Your self-hosted, globally interconnected microblogging community"; inputs.myuids = { url = "path:../myuids"; }; inputs.flake-utils.url = "github:numtide/flake-utils"; inputs.nixpkgs = { url = "github:NixOS/nixpkgs/840c782d507d60aaa49aa9e3f6d0b0e780912742"; flake = false; }; inputs.mastodon = { url = "github:tootsuite/mastodon/v2.9.4"; flake = false; }; outputs = { self, myuids, nixpkgs, mastodon, flake-utils }: flake-utils.lib.eachSystem ["x86_64-linux"] (system: let pkgs = import nixpkgs { inherit system; overlays = []; }; version = (builtins.fromJSON (builtins.readFile ./flake.lock)).nodes.mastodon.original.ref; inherit (pkgs) callPackage; in rec { packages.mastodon = callPackage ./. { src = mastodon // { inherit version; }; }; defaultPackage = packages.mastodon; legacyPackages.mastodon = packages.mastodon; checks = { build = defaultPackage; }; } ) // rec { overlays = { mastodon = final: prev: { mastodon = self.defaultPackage."${final.system}"; }; }; overlay = overlays.mastodon; nixosModule = { lib, pkgs, config, ... }: let name = "mastodon"; cfg = config.immaeServices.mastodon; in { options.immaeServices.mastodon = { enable = lib.mkEnableOption "Enable Mastodon’s service"; user = lib.mkOption { type = lib.types.str; default = name; description = "User account under which Mastodon runs"; }; group = lib.mkOption { type = lib.types.str; default = name; description = "Group under which Mastodon runs"; }; dataDir = lib.mkOption { type = lib.types.path; default = "/var/lib/${name}"; description = '' The directory where Mastodon stores its data. ''; }; socketsPrefix = lib.mkOption { type = lib.types.str; default = "live"; description = '' The prefix to use for Mastodon sockets. ''; }; socketsDir = lib.mkOption { type = lib.types.path; default = "/run/${name}"; description = '' The directory where Mastodon puts runtime files and sockets. ''; }; configFile = lib.mkOption { type = lib.types.path; description = '' The configuration file path for Mastodon. ''; }; package = lib.mkOption { type = lib.types.package; default = pkgs.mastodon; description = '' Mastodon package to use. ''; }; # Output variables workdir = lib.mkOption { type = lib.types.package; default = cfg.package.override { varDir = cfg.dataDir; }; description = '' Adjusted mastodon package with overriden varDir ''; readOnly = true; }; systemdStateDirectory = lib.mkOption { type = lib.types.str; # Use ReadWritePaths= instead if varDir is outside of /var/lib default = assert lib.strings.hasPrefix "/var/lib/" cfg.dataDir; lib.strings.removePrefix "/var/lib/" cfg.dataDir; description = '' Adjusted Mastodon data directory for systemd ''; readOnly = true; }; systemdRuntimeDirectory = lib.mkOption { type = lib.types.str; # Use ReadWritePaths= instead if socketsDir is outside of /run default = assert lib.strings.hasPrefix "/run/" cfg.socketsDir; lib.strings.removePrefix "/run/" cfg.socketsDir; description = '' Adjusted Mastodon sockets directory for systemd ''; readOnly = true; }; sockets = lib.mkOption { type = lib.types.attrsOf lib.types.path; default = { node = "${cfg.socketsDir}/${cfg.socketsPrefix}_node.sock"; rails = "${cfg.socketsDir}/${cfg.socketsPrefix}_puma.sock"; }; readOnly = true; description = '' Mastodon sockets ''; }; }; config = lib.mkIf cfg.enable { nixpkgs.overlays = [ self.overlay ]; users.users = lib.optionalAttrs (cfg.user == name) { "${name}" = { uid = myuids.lib.uids.mastodon; group = cfg.group; description = "Mastodon user"; home = cfg.dataDir; useDefaultShell = true; }; }; users.groups = lib.optionalAttrs (cfg.group == name) { "${name}" = { gid = myuids.lib.gids.mastodon; }; }; systemd.slices.mastodon = { description = "Mastodon slice"; }; systemd.services.mastodon-streaming = { description = "Mastodon Streaming"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" "mastodon-web.service" ]; environment.NODE_ENV = "production"; environment.SOCKET = cfg.sockets.node; path = [ cfg.workdir.nodejs pkgs.bashInteractive ]; script = '' exec npm run start ''; postStart = '' while [ ! -S $SOCKET ]; do sleep 0.5 done chmod a+w $SOCKET ''; postStop = '' rm $SOCKET ''; serviceConfig = { Slice = "mastodon.slice"; User = cfg.user; EnvironmentFile = cfg.configFile; PrivateTmp = true; Restart = "always"; TimeoutSec = 15; Type = "simple"; WorkingDirectory = cfg.workdir; StateDirectory = cfg.systemdStateDirectory; RuntimeDirectory = cfg.systemdRuntimeDirectory; RuntimeDirectoryPreserve = "yes"; }; unitConfig.RequiresMountsFor = cfg.dataDir; }; systemd.services.mastodon-web = { description = "Mastodon Web app"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; environment.RAILS_ENV = "production"; environment.BUNDLE_PATH = "${cfg.workdir.gems}/${cfg.workdir.gems.ruby.gemPath}"; environment.BUNDLE_GEMFILE = "${cfg.workdir.gems.confFiles}/Gemfile"; environment.SOCKET = cfg.sockets.rails; path = [ cfg.workdir.gems cfg.workdir.gems.ruby pkgs.file pkgs.imagemagick ]; preStart = '' install -m 0755 -d ${cfg.dataDir}/tmp/cache ./bin/bundle exec rails db:migrate ''; script = '' exec ./bin/bundle exec puma -C config/puma.rb ''; postStart = '' exec ./bin/tootctl cache clear ''; serviceConfig = { Slice = "mastodon.slice"; User = cfg.user; EnvironmentFile = cfg.configFile; PrivateTmp = true; Restart = "always"; TimeoutSec = 60; Type = "simple"; WorkingDirectory = cfg.workdir; StateDirectory = cfg.systemdStateDirectory; RuntimeDirectory = cfg.systemdRuntimeDirectory; RuntimeDirectoryPreserve = "yes"; }; unitConfig.RequiresMountsFor = cfg.dataDir; }; # To be run manually because computationnally heavy systemd.services.mastodon-cleanup-manual = { description = "Cleanup mastodon"; environment.RAILS_ENV = "production"; environment.BUNDLE_PATH = "${cfg.workdir.gems}/${cfg.workdir.gems.ruby.gemPath}"; environment.BUNDLE_GEMFILE = "${cfg.workdir.gems.confFiles}/Gemfile"; environment.SOCKET = cfg.sockets.rails; path = [ cfg.workdir.gems cfg.workdir.gems.ruby pkgs.file ]; script = '' exec ./bin/tootctl statuses remove --days 365 ''; serviceConfig = { User = cfg.user; EnvironmentFile = cfg.configFile; PrivateTmp = true; Type = "oneshot"; WorkingDirectory = cfg.workdir; StateDirectory = cfg.systemdStateDirectory; RuntimeDirectory = cfg.systemdRuntimeDirectory; RuntimeDirectoryPreserve = "yes"; }; unitConfig.RequiresMountsFor = cfg.dataDir; }; systemd.services.mastodon-cleanup = { description = "Cleanup mastodon"; startAt = "daily"; restartIfChanged = false; environment.RAILS_ENV = "production"; environment.BUNDLE_PATH = "${cfg.workdir.gems}/${cfg.workdir.gems.ruby.gemPath}"; environment.BUNDLE_GEMFILE = "${cfg.workdir.gems.confFiles}/Gemfile"; environment.SOCKET = cfg.sockets.rails; path = [ cfg.workdir.gems cfg.workdir.gems.ruby pkgs.file ]; script = '' exec ./bin/tootctl media remove --days 30 ''; serviceConfig = { User = cfg.user; EnvironmentFile = cfg.configFile; PrivateTmp = true; Type = "oneshot"; WorkingDirectory = cfg.workdir; StateDirectory = cfg.systemdStateDirectory; RuntimeDirectory = cfg.systemdRuntimeDirectory; RuntimeDirectoryPreserve = "yes"; }; unitConfig.RequiresMountsFor = cfg.dataDir; }; systemd.services.mastodon-sidekiq = { description = "Mastodon Sidekiq"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" "mastodon-web.service" ]; environment.RAILS_ENV="production"; environment.BUNDLE_PATH = "${cfg.workdir.gems}/${cfg.workdir.gems.ruby.gemPath}"; environment.BUNDLE_GEMFILE = "${cfg.workdir.gems.confFiles}/Gemfile"; environment.DB_POOL="5"; path = [ cfg.workdir.gems cfg.workdir.gems.ruby pkgs.imagemagick pkgs.ffmpeg pkgs.file ]; script = '' exec ./bin/bundle exec sidekiq -c 5 -q default -q mailers -q pull -q push ''; serviceConfig = { Slice = "mastodon.slice"; User = cfg.user; EnvironmentFile = cfg.configFile; PrivateTmp = true; Restart = "always"; TimeoutSec = 15; Type = "simple"; WorkingDirectory = cfg.workdir; StateDirectory = cfg.systemdStateDirectory; RuntimeDirectory = cfg.systemdRuntimeDirectory; RuntimeDirectoryPreserve = "yes"; }; unitConfig.RequiresMountsFor = cfg.dataDir; }; }; }; }; }