blog/content/posts/2023/01/mastodon-nixos.md

6.6 KiB

title date tags
Containerised Mastodon server on NixOS 2023-01-15
nix
fediverse

I've been on the fediverse on and off since friendi.ca started to be a thing. I remember hosting an instance at ponk.pink that suddenly got popular and the server melted away while I was apologising to the users. I can see someone bought the domain to host some psychedelic My Little Pony stuff, I'm all for that.

I moved to todon.nl when it got started and was quite happy there - check it out if you're looking for an instance ! The itch was there though, what if I host my own small cozy place on the fediverse myself. I knew I didn't have as much time to do server admin, so I decided to start small, with a single user instance an use the power of NixOS to help with maintenance.

Here is my full config - hosting Mastodon under the apex domain of peninsula.industries, Mastodon is running inside a systemd container and the config is using nix-sops to store the secrets. Few things that were unexpected/of note

  • I needed to create the folder structure with correct permissions so that Mastodon starts normally.
  • I'm decrypting secrets on the host and making them available read-only to the container. Not sure if this is better or worse than having sops inside of the container, but I was having some trouble using sops module from there, so left it as it is for now - something to look into in the future
  • I needed to change the postgres port that is running inside of the Mastodon container because I had another postgres on this host already.
  • If you're gonna be playing with this a lot, you might need to remove the container and its data and start from scratch - you can do so by doing:
    • remove the container definition and nixos-rebuild switch
    • rm -fr /var/lib/nixos-containers/mastodon/ *
  • to use Mastodon CLI you need to be running it as a correct user and within the Nix-changed env:
    nixos-container root-login mastodon
    sudo -u mastodon bash
    cd
    source mastodon-env
    RAILS_ENV=production tootctl
    
  • The below can use some refactoring, there is some weird repetition in some places still
  • You can find the newest version of this and see how it's being used in context on my code hosting site.
{
  config,
  pkgs,
  inputs,
  lib,
  ...
}: let
  domain = "peninsula.industries";
  streamingPort = 55000;
  webPort = 55001;
  postgresPort = 5433;
  path = "/var/lib/mastodon/";
  mailgunSmtpSecretName = "mastodon-mailgun-smtp-password";
  mailgunSmtpPasswordPath = "/run/secrets/${mailgunSmtpSecretName}";
  mastodonDbSecretName = "mastodon-db";
  mastodonDbSecretPath = "/run/secrets/${mastodonDbSecretName}";
  uid = 2049;
  gid = 3049;
  systemUserName = "mastodon";
  systemGroupName = "mastodon";
  users = {
    users."${systemUserName}" = {
      inherit uid;
      isSystemUser = true;
      isNormalUser = false;
      group = systemGroupName;
    };
    groups."${systemGroupName}" = {
      inherit gid;
      members = ["${systemUserName}" "nginx"];
    };
  };
  secretSettings = {
    owner = systemUserName;
    group = systemGroupName;
  };
  publicPath = "${path}/public-system/";
  package =
    inputs.nixpkgs-nixos-unstable.legacyPackages."${pkgs.system}".mastodon;
in {
  imports = [../nginx.nix];

  services.nginx = {
    virtualHosts = {
      "${domain}" = {
        forceSSL = true;
        enableACME = true;
        root = "${package}/public/";

        locations."/" = {tryFiles = "$uri @proxy";};
        locations."/system/".alias = "${publicPath}";

        locations."@proxy" = {
          proxyPass = "http://127.0.0.1:" + toString webPort;
          proxyWebsockets = true;
        };
        locations."/api/v1/streaming/" = {
          proxyPass = "http://127.0.0.1:" + toString streamingPort;
          proxyWebsockets = true;
        };
      };
    };
  };

  sops.secrets."${mailgunSmtpSecretName}" =
    {
      sopsFile = ./mailgun.sops.yaml;
      path = mailgunSmtpPasswordPath;
    }
    // secretSettings;
  sops.secrets."${mastodonDbSecretName}" =
    {
      sopsFile = ./mastodon-db.sops.yaml;
      path = mastodonDbSecretPath;
    }
    // secretSettings;

  inherit users;

  systemd.services.mastodon-make-path = {
    script = ''
      mkdir -p ${path}
      chown -R ${systemUserName}:${systemGroupName} ${path}
      mkdir -p ${publicPath}
      chmod -R o-rwx ${publicPath}
      chmod -R g-rwx ${publicPath}
      chmod -R g+X ${publicPath}
      chmod -R g+r ${publicPath}
      chmod -R u+rwX ${publicPath}
    '';
    serviceConfig = {
      Type = "oneshot";
      ProtectSystem = "strict";
      ReadWritePaths = path;
    };
    before = ["container@mastodon.service"];
  };

  containers.mastodon = {
    autoStart = true;
    forwardPorts = [
      {
        containerPort = streamingPort;
        hostPort = streamingPort;
      }
      {
        containerPort = webPort;
        hostPort = webPort;
      }
    ];
    bindMounts = {
      "${path}" = {
        hostPath = "${path}";
        isReadOnly = false;
      };
      "${mailgunSmtpPasswordPath}" = {
        hostPath = "${mailgunSmtpPasswordPath}";
        isReadOnly = true;
      };
      "${mastodonDbSecretPath}" = {
        hostPath = "${mastodonDbSecretPath}";
        isReadOnly = true;
      };
    };
    config = {
      config,
      pkgs,
      lib,
      ...
    }: {
      system.stateVersion = "22.05";
      services.postgresql.port = postgresPort;
      users =
        users
        // {
          mutableUsers = false;
          allowNoPasswordLogin = true;
        };
      services.mastodon = {
        enable = true;
        inherit package;
        localDomain = "${domain}";
        user = systemUserName;
        group = systemGroupName;
        smtp = {
          host = "smtp.eu.mailgun.org";
          port = 465;
          authenticate = true;
          user = "postmaster@${domain}";
          fromAddress = "Peninsula Industries Mastodon <mastodon@${domain}>";
          createLocally = false;
          passwordFile = "${mailgunSmtpPasswordPath}";
        };
        extraConfig = {
          SMTP_TLS = "true";
          SMTP_ENABLE_STARTTLS_AUTO = "true";
          SINGLE_USER_MODE = "true";
          RAILS_SERVE_STATIC_FILES = "true";
        };
        inherit streamingPort;
        inherit webPort;
        configureNginx = false;
        enableUnixSocket = false;
        database = {
          port = postgresPort;
          passwordFile = mastodonDbSecretPath;
        };
      };
    };
  };
}