关于部署一个Mastodon站点

我是如何写作本文的:在应用部署平台和 VPS 上各自建立了一个测试用的 Mastodon 站点后,作技术笔记。自评可靠程度:可靠性中等,全面性中等。

本文适合谁看/怎么看:了解 Mastodon 的设计和使用,并对建立自己的 Mastodon 站点感兴趣的读者。单纯想看关于 Mastodon 的小介绍、VPS 评测、或 Nginx 笔记Docker 教程 亦可。

更新策略:我可能会直接修改本文内容,而不标注”X月X日更新“

我对 Mastodon 的理解

Mastodon,直译为”乳齿象”,是一个去中心化的、开放源代码的社交平台,是由一个个自主运营、规则各异的站点形成的邦联。虽然理念背道而驰,但 Mastodon 的用法比较像 Twitter (X),所以某种程度上和 Twitter (X) 形成竞争用户的关系。

了解 Mastodon 运行机制的最好办法是加入一个 Mastodon 站点(又称成为“象友”)。通过发布嘟文(Toot),你可以了解到 Mastodon 的诸多规则,比如“跨站可见性”的含义、站点一般默认禁用全局文本搜索的选择、打Hash Tag的作用等等。

从 Managed Service 到 Self Hosting 的光谱

假设某一天,你想要搭建、维护一个自己的 Mastodon 站点,服务一个用户群体,成为一个自主运营的站点邦联中的一个,暂时忽略设定站点规则等社会方面,在技术的层面如何实施?你会发现有很多技术路线可选。一端是开箱即用、简化建站维护流程的托管服务(Managed Service),一端是自主可控、支持定制的自托管(Self Hosting)。

最省事的办法是把搭建和维护站点的责任委托给服务商。这篇博文详细介绍了托管站服务,Mastodon的官方文档中列出了一些托管商,如 masto.host 等,这些托管商的共同特点是它们都专注于提供 Mastodon 等类似的社交平台建站服务。

稍微灵活一些的托管方案是使用一些通用的应用部署平台,比如 railway.app,比如 cloud.sealos.io。在这些平台上,你可以依据一个 docker image、一个 GitHub 仓库或一个本地代码仓库来建立应用,从 ChatGPT 的网页 UI,到依赖 PostgreSQL 和 Redis 的 Mastodon 应用,不一而足。例如 railway 就会通过你提供的代码来建立一个符合 OCI 标准的镜像。这一方案允许你对站点进行代码级更改,但仍有一些限制,如 railway 就不支持 docker-compose

比较经典的自托管解决方案是租用服务器 VPS,建立站点。你能够通过命令行接入服务器,从操作系统的层面全面控制服务器,从而定制你的站点。具体到 Mastodon 上,可以依照官方文档的教程一步步操作安装软件,也可以尝试使用 Docker 镜像简化流程。总体而言,一个 Mastodon 实例需要 2G 往上的内存和 25G 往上的硬盘空间。

自托管的终极方案是使用自己手头的主机作为服务器,你将需要负责所有技术细节,对站点有完全的掌控。

在应用部署平台试水

Disclaimer:我之所以尝试使用 railway 部署,是因为我之前订阅了 railway 的服务,不代表 railway 是干这件事情的一个好的/成熟的解决方案。

搜索 “deploy mastodon to railway”,得到这个现成的仓库。按照仓库的说明填上用户名 (不要选为 “admin”,会和保留字段冲突) 和邮箱。如果出现问题,就用重启解决一切问题(实际上是各个服务启动的顺序很关键,重启 mastodon 服务之前/同时可能需要重启其它服务)。最终可以成功进入 Mastodon 站点。

部署结束后反思,这个方案有诸多缺点:提供的控制不够,对服务器没有命令行控制,不支持docker-compose。况且,该平台按CPU、内存、出站流量按量加收使用费,由于Mastodon占用大量内存,平台的预计收费为$10每月,性价比很低。

VPS 的选择

既然应用部署平台不是好的解决方案,考虑租用VPS。

这里有一个有意思的对比:Amazon, Microsoft 和 Google 分别推出的 AWS, Azure 和 GCP(合称 hyperscaler cloud)和其它 VPS(如 Digital Ocean, Vulture 等)。前者可以定性为“云服务”,扩展性极强,面向企业提供高品质服务,资费较贵;后者可以定性为“租用服务器”,面向个人和小商户,性价比高。

具体而言,前者对出站流量(Egress traffic)累计计费,收费项目名目繁多,金额上不封顶,采用后付费的账单制,可能出现天价账单。AWS EC2 出站流量收费 $0.09/GB,即 $90/TB。与之相对的,一些专门的VPS如DigitalOcean, Contabo, 等等都采取“包月”制,每月预付费固定的金额,如果出站流量超限则限速,不会出现天价账单。如果硬要比较出站流量的价格,Contabo每月$5.5的套餐允许高达32TB的出站流量,要便宜数个数量级。

在 VPS 上部署 Mastodon 的技术路线

就我目前了解,有两条技术路线:一条是按照官网的教程一步一步走;另一条是在 Docker 中部署,这个官网的技术支持可能不多。出于个人原因,我选择了第二条路线。

本篇博文的余下部分会先概述用到的两个工具 Nginx 和 Docker,再给出一个大致的部署步骤和运维方法。

相关技术笔记其一:Nginx 的配置

静态网页

服务器nginx的基本配置可以参考其教程。也最好参考/etc/nginx/sites-available/default中的内容。关于设置每个端口的default_server,见这里

对于静态网页的服务,基本的原理为:

server {
    location / {
        root /data/www;
    }

    location /images/ {
        root /data;
    }
}
# request example.com/images/01.png will be mapped to /data/images/01.png 
# request example.com/index.html will be mapped to /data/www/index.html on the server's filesystem

简而言之,用户请求的URI会被用来和每个location做前缀匹配,选出最长者,把URI中的path加到root后面。

这个配置里之所以不用listen,是因为使用默认的80端口。

进行代理

server {
    location / {
        proxy_pass http://localhost:8080/;
    }

    location ~ \.(gif|jpg|png)$ {
        root /data/images;
    }
}
#  preceded with ~ is regular expression

在这个例子中,所有义.gif .jpf .png为后缀的文件请求会被映射到/data/images,其余的请求会被转发给8080端口上被代理的服务器。

相关技术笔记其二:Docker 的使用

基本概念:image和container

首先,需要区分镜像image和容器container

镜像是一个快照或者模板,定义了如何创造对应的容器。一个容器可以包含很多“层”,进行修改相当于在顶上添加一层

容器是镜像的实例,依据一个镜像可以创造出多个对应的容器(docker run 命令)。

有若干方法创造新的镜像,比如 1) 使用 Dockerfile,使用命令如 docker build -t test:latest . ;2) 利用对容器所做的变化定义镜像,使用 docker commit <container_id> <new_image_name>

一个示例的 Dockerfile:

# syntax=docker/dockerfile:1
FROM ubuntu:22.04

# install app dependencies
RUN apt-get update && apt-get install -y python3 python3-pip
RUN pip install flask==3.0.*

# install app
COPY hello.py /

# final configuration
ENV FLASK_APP=hello
EXPOSE 8000
CMD ["flask", "run", "--host", "0.0.0.0", "--port", "8000"]

在上面的示例中,如果使用docker build -t test:latest .来构建镜像,则要求该 Dockerfile 本身和 hello.py 在当前文件夹中。

基础用法

docker创建容器的例子:

docker run --name mypostgres14 -v /opt/mastodon/postgres14:/var/lib/postgresql/data -e   POSTGRES_PASSWORD=password --rm -d postgres:14-alpine

--name: assign a name to the container
-v: bind mount a volume
-e: set environment variables
-d: detach, run container in background
--rm: Automatically remove the container and its associated anonymous volumes when *it exits*, *not when this command exits*
the postgres:14-alpine is the IMAGE argument

docker在列出所有容器/镜像:

docker ps -a
docker image list

docker在运行中的容器里执行命令的例子:

docker exec -it mypostgres14 psql -U postgres

-i: interactive, -t: pseudo-tty
mypostgre14 is the container name
psql -U postgres is the command and arguments to pass to the container

docker查看容器log:

docker logs --tail 50 --follow --timestamps CONTAIER-NAME

docker删除容器/镜像:

docker container rm CONTAINER
docker container prune 
# removes all stopped containers
docker image rm IMAGE

docker compose 的用法

这里列举一个 docker compose 的一个基础使用示例,请详细研读,了解docker compose 在干什么。注:后文将compose中的服务和容器交换使用了,但服务和容器不一定一一对应,请注意甄别。

在docker-compose.yaml的Service元素中,每个service都要么有一个build章节定义如何构建,要么有一个image章节标明使用哪个现成的镜像。

按顺序构建并运行所有服务所需的容器:docker compose up -d

停止所有容器:docker compose stop
删除所有用到的容器:docker compose down
在新创建一个服务,并在新创建的服务上运行命令:docker compose run SERVICE

深入理解 Docker 中的网络机制

network是docker中的一等公民。可以通过 docker network ls 来查看所有网络,通过 docker network inspect NAME 来检查其中某一个网络。

docker-compose.yaml中network含义的文档。默认地,compose会为你的app创建一个network,连接到同一个Network的服务可以互相通信。具体如何通信?前面提到的基础使用示例中,服务web想要找服务redis通信,在网络的层面使用hostname为”redis”(其它的选择包括”localhost”, “example.com”等等),可见dockers的每个network中实际上有一个DNS系统,把服务名映射到主机名。

=== docker_compose.yaml ===
services:
  web:
    build: .
    ports:
      - "8000:5000"
  redis:
    image: "redis:alpine"

=== app.py for web ===
import redis
cache = redis.Redis(host='redis', port=6379)

后续填写 Mastodon 的 .env.production 时,会需要填写”Name of PostgreSQL database:“、”Redis host:“等字段。这其实和刚才的例子是相通的,填写 docker-compose.yaml 里数据库、Redis服务的名称即可。

深入理解 Dockerfile 的命令

RUN 命令:构建镜像过程中执行的命令。如果想要看到它的输出,在 docker build 命令中加命令行参数 --progress=plain

CMD 命令:定义了依据该镜像启动容器时,容器中第一时间执行的命令。如果在 docker run 时传入了 [COMMAND] 参数,则 CMD 定义的命令会被覆盖。

ENTRYPOINT 命令:与 CMD 命令类似,但是不会被覆盖,运行时传入的命令会被追加(append)在这里定义的命令之后执行(参考)。例如,这个 Dockerfile 中有一行 ENTRYPOINT ["/usr/bin/tini", "--"] ,而对应的 docker-compose.yaml 中 web 服务的命令为 command: bash -c "rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000",这就导致 docker ps -a 检查得到的 web 服务正在运行的命令为 /usr/bin/tini -- ba…

搭建过程概述

Mastodon 的代码仓库有不少分支,比如 glitch-soc,这里出于个人原因尝试部署一个名为”闭社“的代码仓库,它是 glitch-soc 的分支(所以是分支的分支),不过不论部署哪个分支,其原理是相通的。

第一次搭建时互补参考了(1), (2)

配置 Nginx

参考上面的 Nginx 笔记和搭建博客来配置和调试,不在这里详细描述了。

配置Postgres数据库

不做这步,在docker-compose.yml的db service下写如下配置

environment:
      POSTGRES_DB: mastodon_production
      POSTGRES_USER: mastodon
      POSTGRES_PASSWORD: change_me

可能也行

试着连接数据库的命令行

在下载完毕所需软件后(假设闭社的源码解压在了/opt/mastodon/下),首先利用容器在/opt/mastodon/postgres14路径下创建一个数据库:

docker run --name mypostgres14 -v /opt/mastodon/postgres14:/var/lib/postgresql/data -e   POSTGRES_PASSWORD=password --rm -d postgres:14-alpine

命令会立刻返回,此时多出一个名为mypostgres14的容器,–rm是指容器自己退出时删除。接着,

docker exec -it mypostgres14 psql -U postgres

这里 psql -U postgres 是在容器内执行的命令,postgres是默认的super user。按下回车键后,会进入容器内的Postgres的命令行。

如果 /opt/mastodon/postgres14 处已经有了数据库,那么可以尝试输入 \l(相当于MySQL的 show databases;) 来列出所有数据库,\c database_name 来连接到任意一个数据库,\dt 来列出该数据库的所有表 (相当于MySQL的 show tables;),通过这种方式检查mastodon的数据库使用情况。

如果是第一次搭建,则可以执行 CREATE USER mastodon WITH PASSWORD 'password' CREATEDB; 来为Mastodon创建一个用户,这里 CREATEDB 指赋予新建用户自主创建数据库的权限。 然后 \q 退出,退出命令行后容器仍未退出(因为postgres的镜像中已经定义了一个独立于用户的server process在始终运行)。

数据是存在容器之外

这里需要注意的是,数据是存在容器之外的(即 /opt/mastodon/postgres14 路径下),不会随容器的销毁而销毁。事实上,这里创造的mypostgres14这个容器不会再被使用了,观察 docker-compose.yml 中的 db Service,它也是把容器之外的路径 /opt/mastodon/postgres14 映射到容器内文件系统的。

db:
    restart: always
    image: postgres:14-alpine
    shm_size: 256mb
    networks:
      - internal_network
    healthcheck:
      test: ['CMD', 'pg_isready', '-U', 'postgres']
    volumes:
      - ./postgres14:/var/lib/postgresql/data
    environment:
      - 'POSTGRES_HOST_AUTH_METHOD=trust'

填写 Mastodon 的配置文件 .env.production

依据上面所做的数据库配置(Mastodon用户的用户名和密码)、观察docker-compose.yaml的内容,以及对docker网络机制的了解,你应该可以填写 .env.production 了。关于发送邮件,目前可以先填写从localhost发送,后续等站点上线后再更改。

创建自定义的Mastodon镜像

仔细观察docker-compose.yml,发现需要启动5个容器/服务:

  1. db、redis:直接拉取镜像。文件定义了它们的healthcheck(健康检查方法),environment(环境变量), volumes(文件系统映射)等等。如前所述,镜像中已经定义了一个server process,因此我们不需要定义command来覆盖镜像中默认执行的CMD(参考)。
  2. Web、Streaming、Sidekiq:它们的执行环境基于本地的 Dockerfile 构建(build .),鉴于Dockerfile中有复制源码到容器中的操作,因此它们相当于基于本地的代码。另外,每个服务中需要定义一个特有的command,来规定容器被创建时该执行哪个命令,真正启动该服务。

根据我的记忆,有一次服务崩溃时,web, streaming和sidekiq会显示exited,而db和redis仍在running,印证了上面的说法(退出的是mastodon相关的命令)。

如果让Web、Streaming、Sidekiq三个服务其中的每一个都构建自己的镜像(在docker-compose.yaml中,使用列出 build . 而不列出 image: )则每个服务都会产生一个自己的镜像,一来花费三倍的源码编译时间,二来总镜像文件大小要乘以三。不如手动构建并在本地保存三者共用的镜像:

docker build -t my-closed-social .

在docker-compose.yaml的web, streaming, sidekiq三个服务中,进行如下改动

<<<<<<<
build: .
image: ghcr.io/mastodon/mastodon
=======
image: my-closed-social
>>>>>>>

三个服务利用同一个本地构建的镜像。如果不做这个改动,行为上可能相似,但配置文件中既有build又有image,compose,按照 Docker 的文档,其行为不好预期。

至此,你的 Mastodon 站点应该可以通过域名访问了,而你应该可以作为管理员登入你的 Mastodon 站点。

开放新用户注册与运维

参考:

配置邮件服务帮助新用户注册

新用户注册时,Mastodon 站点需要向用户提供的邮箱发送验证邮件,这需要一个邮件服务。

首先,作为管理员登入系统,在Administration>Server Settings>Registrations 中,控制是否允许新用户注册。

然后,创建一个邮件服务。为了方便,可以考虑使用第三方邮件服务器,如Zoho Mail,然后在 .env.production 中填写 SMTP_ 开头的配置,重新构建镜像、上线服务。

运维:使用 tootctl 命令

docker compose run --rm web bin/tootctl

创建tootctl.sh文件,使用alias将tootctl设置为tootctl.sh的别名。在tootctl.sh文件中将传入的参数传入容器中的bin/tootctl即可。具体需要执行的命令参考这个文档


Posted

in

by

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *