Fading Bits

Mastodon 部署小记

Self-hosting 有瘾。班也不上了学也不上了就天天盯着你那 SSH。

准备工作

Mastodon 大致需要如下组件:

  • Mastodon 核心服务:前端(web)和后端(streaming)
  • PostgreSQL
  • Redis
  • SMTP
  • (可选)S3 对象存储

得益于伟大的 NixOS 生态,前三者基本可以自动配置。实际上 SMTP 也有相应的 NixOS module,但为了可控性,笔者通过 nixos-mailserver 托管一套完整的邮局方案(开箱即用,此处略过)。

实际部署

{
  config,
  lib,
  pkgs,
  ...
}:
let
  domain = "m.tsubasa.moe";
in
{
  sops.secrets = {
    "mail-users/mastodon" = {
      owner = config.services.mastodon.user;
      sopsFile = ../mailserver/secrets.yaml;
    };
    "mastodon-s3" = {
      owner = config.services.mastodon.user;
      sopsFile = ./secrets.yaml;
    };
  };

  services.mastodon = {
    enable = true;

    localDomain = domain;

    enableUnixSocket = true;
    configureNginx = false;

    streamingProcesses = 3;

    smtp = {
      createLocally = false;

      authenticate = true;
      host = "mail.tsubasa.moe";
      port = 587;
      user = "mastodon@tsubasa.moe";
      passwordFile = config.sops.secrets."mail-users/mastodon".path;
      fromAddress = "mastodon@tsubasa.moe";
    };

    extraConfig = {
      WEB_DOMAIN = domain;
    };

    extraEnvFiles = [
      config.sops.secrets."mastodon-s3".path
    ];
  };

  # https://github.com/NixOS/nixpkgs/blob/nixos-unstable/nixos/modules/services/web-apps/mastodon.nix
  services.nginx =
    let
      cfg = config.services.mastodon;
    in
    {
      enable = true;
      recommendedProxySettings = true;

      virtualHosts."${domain}" = {
        listen = [
          {
            addr = "127.0.0.1";
            port = config.lib.ports.mastodon;
            ssl = false;
          }
        ];

        root = "${cfg.package}/public/";

        # let cloudflared handle TLS
        forceSSL = false;
        enableACME = false;

        locations."/system/".alias = "/var/lib/mastodon/public-system/";

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

        locations."@proxy" = {
          proxyPass = (
            if cfg.enableUnixSocket then
              "http://unix:/run/mastodon-web/web.socket"
            else
              "http://127.0.0.1:${toString cfg.webPort}"
          );
          proxyWebsockets = true;

          extraConfig = ''
            proxy_set_header X-Forwarded-Proto https;
          '';
        };

        locations."/api/v1/streaming/" = {
          proxyPass = "http://mastodon-streaming";
          proxyWebsockets = true;
        };
      };

      upstreams.mastodon-streaming = {
        extraConfig = ''
          least_conn;
        '';
        servers = builtins.listToAttrs (
          map (i: {
            name = "unix:/run/mastodon-streaming/streaming-${toString i}.socket";
            value = { };
          }) (lib.range 1 cfg.streamingProcesses)
        );
      };
    };

  users.groups.${config.services.mastodon.group}.members = [
    config.services.nginx.user
  ];
}

这里整理一些的经验:

WEB_DOMAINLOCAL_DOMAIN

Mastodon 允许将用户 handle 置于和提供服务的网页不同的域名下。例如,user@example.org 可能存在于 mastodon.example.org 的 Mastodon 服务下。

根据 官方文档,设置 WEB_DOMAIN=mastodon.example.orgLOCAL_DOMAIN=example.org,并在 example.org/.well-known/webfinger 进行 301 重定向即可。

实际上,包括官方安卓客户端在内许多客户端都没有正确处理这种情况

一些过时的文档中提到,需要在 host-meta 等路径反代。推测是历史遗留设计。

  // tsubasa.moe 是一个 cloudflare worker
  if (path.startsWith("/.well-known/webfinger") ||
      path.startsWith("/.well-known/host-meta") ||
      path.startsWith("/.well-known/nodeinfo") ||
      path.startsWith("/@")) {
    return new Response(null, {
      status: 301,
      headers: {
        'Location': `https://mastodon.tsubasa.moe${url.pathname}${url.search}`,
        'Access-Control-Allow-Origin': '*'
      }
    });
  }

上面的代码只解决了一部分行为。

因为无法确定这些错误是无关紧要的前端行为,还是后端的数据混乱,笔者放弃了这种部署方式。

反代处理

这台机器大部分服务通过 Cloudflared 提供。但是 Mastodon 需要在多个不同路径反代或提供文件服务,Cloudflared 稍显羸弱了。

这里引入了一个 Nginx 作为中间层。Cloudflared 直接反代 Nginx 的 HTTP 端口,由 Nginx 处理复杂的反代需求。

由于 ActivityPub 的协议依赖于 HTTPS,在通过 Nginx 向 Mastodon 发起 HTTP 连接时会被重定向到 HTTPS。显然这个请求会回到 Cloudflared,后者继续发起 HTTP 连接,陷入无限重定向之中。

这里需要给 Nginx 加上 proxy_set_header X-Forwarded-Proto https「欺骗」Mastodon 后端,避免重定向的发生。(参考

S3 对象存储

Mastodon 默认在本地文件系统存储媒体文件。~~显然这不能满足我们的企业级需求。~~

笔者部署了便宜大腕的 Blackblaze B2,并通过 Cloudflare CDN 提供媒体。作为带宽联盟成员,Blackblaze B2 在通过 Cloudflare CDN 提供时不计出口费用。另一个好处是,客户端不再能看见 bucket 的名称。

Thoughts? Leave a comment