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_DOMAIN 与 LOCAL_DOMAIN
Mastodon 允许将用户 handle 置于和提供服务的网页不同的域名下。例如,user@example.org 可能存在于 mastodon.example.org 的 Mastodon 服务下。
根据 官方文档,设置 WEB_DOMAIN=mastodon.example.org 和 LOCAL_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 的名称。