{ flake, config, pkgs, lib, ... }: let inherit (flake.config.people) user0; inherit (flake.config.services) instances; serviceCfg = flake.config.services.instances.mastodon; smtpCfg = flake.config.services.instances.smtp; hostCfg = flake.config.services.instances.web; host = serviceCfg.domains.url0; dns0 = instances.web.dns.provider0; dns0Path = "dns/${dns0}"; fedifetcherConfig = pkgs.writeText "fedifetcher-config.json" ( builtins.toJSON { server = "https://${host}"; home-timeline-length = 200; max-followings = 80; from-notifications = 1; max-bookmarks = 80; max-favourites = 40; backfill-with-context = 1; backfill-mentioned-users = 1; remember-users-for-hours = 168; remember-hosts-for-days = 30; http-timeout = 5; lock-hours = 24; log-level = "INFO"; } ); fedifetcherPython = pkgs.python3.withPackages ( ps: with ps; [ requests pytz beautifulsoup4 certifi charset-normalizer defusedxml docutils idna iniconfig packaging pluggy pytest python-dateutil requests six smmap urllib3 xxhash ] ); fedifetcherSrc = pkgs.fetchFromGitHub { owner = "nanos"; repo = "FediFetcher"; rev = "main"; hash = "sha256-/J7psV/mA7okuuO7/aXVVWS9p63eMncG2CEEGN38ip0="; }; in { # If you need to start fresh for some reason, run these to create the new Admin account: # sudo -u mastodon mastodon-tootctl accounts create nick --email=nick@localhost --confirmed --role=Owner # sudo -u mastodon mastodon-tootctl accounts approve nick # If you fuck up and lose the password, use this: # sudo mastodon-tootctl accounts modify --reset-password nick # If you really fuck up and name yourself wrong, use this shit # sudo mastodon-tootctl accounts modify username --remove-role nixpkgs.overlays = [ (final: prev: { mastodon = prev.mastodon.overrideAttrs (oldAttrs: { patches = (oldAttrs.patches or [ ]) ++ [ ./config/chars.patch ]; }); }) ]; microvm.vms = { ${serviceCfg.name} = { autostart = true; restartIfChanged = true; config = { system.stateVersion = "24.05"; time.timeZone = "America/Winnipeg"; users.users.root.openssh.authorizedKeys.keys = flake.config.people.users.${user0}.sshKeys; services = { ${serviceCfg.name} = { enable = true; localDomain = host; secretKeyBaseFile = "/etc/mastodon-secrets/pass"; streamingProcesses = 7; trustedProxy = hostCfg.localhost.address0; automaticMigrations = true; database = { createLocally = true; name = serviceCfg.name; host = "/run/postgresql"; user = serviceCfg.name; passwordFile = "/etc/mastodon-secrets/database"; }; extraConfig = { SINGLE_USER_MODE = "false"; SMTP_AUTH_METHOD = "plain"; SMTP_DELIVERY_METHOD = "smtp"; SMTP_ENABLE_STARTTLS_AUTO = "true"; SMTP_SSL = "false"; }; # if you're starting from scratch, you gotta cd into /var/lib/mastodon and run: # sudo -u mastodon mastodon-tootctl search deploy elasticsearch = { preset = "single_node_cluster"; host = hostCfg.localhost.address0; port = 9200; }; mediaAutoRemove = { enable = true; olderThanDays = 14; }; redis = { createLocally = true; enableUnixSocket = true; }; sidekiqThreads = 25; sidekiqProcesses = { all = { jobClasses = [ ]; threads = null; }; default = { jobClasses = [ "default" ]; threads = 5; }; ingress = { jobClasses = [ "ingress" ]; threads = 5; }; push-pull = { jobClasses = [ "push" "pull" ]; threads = 5; }; mailers = { jobClasses = [ "mailers" ]; threads = 5; }; }; smtp = { authenticate = true; createLocally = false; fromAddress = "upRootNutrition <${smtpCfg.email.address1}>"; host = smtpCfg.hostname; passwordFile = "/etc/mastodon-secrets/smtp"; port = smtpCfg.ports.port1; user = smtpCfg.email.address1; }; }; opensearch.enable = true; caddy = { enable = true; virtualHosts = { ":80" = { extraConfig = '' handle_path /system/* { file_server * { root /var/lib/mastodon/public-system } } handle /api/v1/streaming/* { reverse_proxy unix//run/mastodon-streaming/streaming.socket { header_up X-Forwarded-Proto {http.request.header.X-Forwarded-Proto} header_up X-Forwarded-Host {http.request.header.X-Forwarded-Host} } } route * { file_server * { root ${pkgs.mastodon}/public pass_thru } reverse_proxy * unix//run/mastodon-web/web.socket { header_up X-Forwarded-Proto {http.request.header.X-Forwarded-Proto} header_up X-Forwarded-Host {http.request.header.X-Forwarded-Host} } } handle_errors { root * ${pkgs.mastodon}/public rewrite 500.html file_server } encode gzip header /* { Strict-Transport-Security "max-age=31536000;" } header /emoji/* Cache-Control "public, max-age=31536000, immutable" header /packs/* Cache-Control "public, max-age=31536000, immutable" header /system/accounts/avatars/* Cache-Control "public, max-age=31536000, immutable" header /system/media_attachments/files/* Cache-Control "public, max-age=31536000, immutable" ''; }; }; }; postgresql = { enable = true; }; openssh = { enable = true; settings = { PasswordAuthentication = false; PermitRootLogin = "prohibit-password"; }; }; }; users.users = { ${serviceCfg.name}.extraGroups = [ "postgres" ]; caddy.extraGroups = [ serviceCfg.name ]; fedifetcher = { isSystemUser = true; group = "fedifetcher"; home = "/var/lib/fedifetcher"; createHome = true; }; }; users.groups.fedifetcher = { }; networking.firewall.allowedTCPPorts = [ 22 # SSH 80 # Caddy 25 # SMTP 139 # SMTP 587 # SMTP 2525 # SMTP 5432 # Postgres ]; systemd = { services = { mastodon-init-dirs.serviceConfig.PrivateMounts = lib.mkForce false; mastodon-web.serviceConfig.PrivateMounts = lib.mkForce false; mastodon-streaming-1.serviceConfig.PrivateMounts = lib.mkForce false; mastodon-streaming-2.serviceConfig.PrivateMounts = lib.mkForce false; mastodon-streaming-3.serviceConfig.PrivateMounts = lib.mkForce false; mastodon-streaming-4.serviceConfig.PrivateMounts = lib.mkForce false; mastodon-streaming-5.serviceConfig.PrivateMounts = lib.mkForce false; mastodon-streaming-6.serviceConfig.PrivateMounts = lib.mkForce false; mastodon-streaming-7.serviceConfig.PrivateMounts = lib.mkForce false; mastodon-sidekiq-all.serviceConfig.PrivateMounts = lib.mkForce false; mastodon-sidekiq-default.serviceConfig.PrivateMounts = lib.mkForce false; mastodon-sidekiq-ingress.serviceConfig.PrivateMounts = lib.mkForce false; mastodon-sidekiq-mailers.serviceConfig.PrivateMounts = lib.mkForce false; mastodon-sidekiq-push-pull.serviceConfig.PrivateMounts = lib.mkForce false; mastodon-elastic-search = { description = "Recache elastic search"; after = [ "network-online.target" "mastodon-web.service" ]; wants = [ "network-online.target" ]; serviceConfig = { WorkingDirectory = "/var/lib/${serviceCfg.name}"; Type = "oneshot"; }; script = '' ${pkgs.mastodon}/bin/mastodon-tootctl search deploy --only-mapping --only=instances accounts tags statuses public_statuses ''; }; mastodon-copy-secrets = { description = "Copy secrets from virtiofs to local filesystem"; before = [ "mastodon-init-dirs.service" ]; requiredBy = [ "mastodon-init-dirs.service" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; script = '' mkdir -p /etc/mastodon-secrets cp /run/secrets/pass /etc/mastodon-secrets/pass cp /run/secrets/database /etc/mastodon-secrets/database cp /run/secrets/redis /etc/mastodon-secrets/redis cp /run/secrets/smtp /etc/mastodon-secrets/smtp cp /run/secrets/fedifetcher-token /etc/mastodon-secrets/fedifetcher-token chmod 755 /etc/mastodon-secrets chmod 644 /etc/mastodon-secrets/* ''; }; fedifetcher = { description = "FediFetcher - Fetch missing posts for Mastodon"; after = [ "network-online.target" "mastodon-web.service" ]; wants = [ "network-online.target" ]; serviceConfig = { Type = "oneshot"; User = "fedifetcher"; Group = "fedifetcher"; WorkingDirectory = "/var/lib/fedifetcher"; TimeoutStartSec = "300"; PrivateTmp = true; NoNewPrivileges = true; ProtectSystem = "strict"; ProtectHome = true; ReadWritePaths = "/var/lib/fedifetcher"; ExecStart = let script = pkgs.writeShellScript "fedifetcher-run" '' set -e # Wait for Mastodon to be fully ready for i in {1..30}; do if ${pkgs.curl}/bin/curl -sf http://localhost:80/health >/dev/null 2>&1; then echo "Mastodon is ready" break fi echo "Waiting for Mastodon to be ready... ($i/30)" sleep 2 done export ACCESS_TOKEN=$(cat /etc/mastodon-secrets/fedifetcher-token) ${fedifetcherPython}/bin/python ${fedifetcherSrc}/find_posts.py \ -c=${fedifetcherConfig} \ --access-token="$ACCESS_TOKEN" ''; in "${script}"; }; }; mastodon-init-db.serviceConfig.EnvironmentFile = "/var/lib/mastodon/.secrets_env"; systemd-tmpfiles-setup.after = [ "var-lib-mastodon.mount" ]; opensearch-install-plugins = { description = "Install OpenSearch plugins"; before = [ "opensearch.service" ]; requiredBy = [ "opensearch.service" ]; serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; script = '' PLUGIN_DIR="/var/lib/opensearch/plugins/analysis-icu" if [ ! -d "$PLUGIN_DIR" ]; then # Create the plugins directory if it doesn't exist mkdir -p /var/lib/opensearch/plugins # Install using the proper OpenSearch plugin command export OPENSEARCH_JAVA_HOME="${pkgs.jdk17}/lib/openjdk" ${pkgs.opensearch}/bin/opensearch-plugin install --batch analysis-icu || { echo "Plugin installation failed, but continuing anyway" exit 0 } fi ''; }; }; timers = { mastodon-elastic-search = { description = "Timer for Mastodon elastic search recaching"; wantedBy = [ "timers.target" ]; timerConfig = { OnBootSec = "10min"; OnUnitActiveSec = "60min"; Unit = "mastodon-elastic-search.service"; }; }; fedifetcher = { description = "Timer for FediFetcher"; wantedBy = [ "timers.target" ]; timerConfig = { OnBootSec = "10min"; OnUnitActiveSec = "15min"; Unit = "fedifetcher.service"; Persistent = true; AccuracySec = "1min"; }; }; }; network = { enable = true; networks."20-lan" = { matchConfig.Name = "enp0s6"; addresses = [ { Address = "${serviceCfg.interface.ip}/24"; } ]; routes = [ { Destination = "${hostCfg.localhost.address1}/0"; Gateway = serviceCfg.interface.gate; } ]; dns = [ "1.1.1.1" "8.8.8.8" ]; }; }; tmpfiles.rules = [ "d /var/lib/mastodon 0755 mastodon mastodon -" "Z /var/lib/mastodon 0755 mastodon mastodon -" "Z /var/lib/postgresql 0755 postgres postgres -" "d /var/cache/mastodon/precompile 0755 mastodon mastodon -" "d /var/lib/mastodon/public-system 0755 mastodon mastodon -" "d /var/lib/mastodon/public-system/accounts 0755 mastodon mastodon -" "d /var/lib/mastodon/public-system/media_attachments 0755 mastodon mastodon -" "d /var/lib/mastodon/public-system/media_attachments/files 0755 mastodon mastodon -" "d /var/lib/mastodon/public-system/site_uploads 0755 mastodon mastodon -" "d /var/lib/fedifetcher 0755 fedifetcher fedifetcher -" ]; }; environment.systemPackages = builtins.attrValues { inherit fedifetcherPython ; inherit (pkgs) bottom ; }; microvm = { vcpu = 2; mem = 1024 * 6; hypervisor = "qemu"; interfaces = [ { type = "tap"; id = serviceCfg.interface.id; mac = serviceCfg.interface.mac; } { type = "user"; id = serviceCfg.interface.idUser; mac = serviceCfg.interface.macUser; } ]; forwardPorts = [ { from = "host"; host.port = serviceCfg.interface.ssh; guest.port = 22; } ]; shares = [ { mountPoint = "/nix/.ro-store"; proto = "virtiofs"; source = "/nix/store"; tag = "read_only_nix_store"; } { mountPoint = "/var/lib/${serviceCfg.name}"; proto = "virtiofs"; source = "${serviceCfg.mntPaths.path0}/data"; tag = "${serviceCfg.name}_data"; } { mountPoint = "/var/lib/postgresql"; proto = "virtiofs"; source = "${serviceCfg.mntPaths.path0}/database"; tag = "${serviceCfg.name}_database"; } { mountPoint = "/run/secrets"; proto = "virtiofs"; source = "/run/secrets/${serviceCfg.name}"; tag = "host_secrets"; } ]; }; }; }; }; sops = { secrets = builtins.listToAttrs ( map (secret: { name = "${serviceCfg.name}/${secret}"; value = { owner = "root"; group = "root"; mode = "0644"; }; }) [ "smtp" "database" "redis" "pass" "fedifetcher-token" ] ); }; systemd.tmpfiles.rules = [ "d ${serviceCfg.mntPaths.path0} 0751 microvm wheel - -" "d ${serviceCfg.mntPaths.path0}/data 0751 microvm wheel - -" "d ${serviceCfg.mntPaths.path0}/database 0751 microvm wheel - -" ]; services.caddy.virtualHosts."${host}" = { extraConfig = '' reverse_proxy http://${serviceCfg.interface.ip}:80 { header_up X-Forwarded-Proto {scheme} header_up X-Real-IP {remote_host} header_up X-Forwarded-For {remote_host} } tls ${serviceCfg.ssl.cert} ${serviceCfg.ssl.key} encode zstd gzip ''; }; users.users.caddy.extraGroups = [ "acme" ]; security.acme.certs."${host}" = { dnsProvider = dns0; environmentFile = config.sops.secrets.${dns0Path}.path; group = "caddy"; }; }