一个十人左右的团队,每天在聊天软件里产生几百条消息、几十个文件,还有无数语音沟通。数据存在别人的服务器上,说实话没有真正的自主权。我自己也是从通用商业聊天工具里挣扎出来的,试过各种临时凑合的自建方案,最后稳定跑起来的是一套基于 Matrix 协议的组合:服务端用 Synapse,网页端用 Element,移动端用 FluffyChat。这篇就是我实际部署的记录,包含完整步骤、配置文件、客户端接入方式,还有我踩过的三个隐蔽问题。
如果你正在纠结要不要自建沟通协作系统,建议先把这篇文章前半部分的规划看完再动手。Matrix 这个协议有一个很大的特点:只要域名不变,服务端可以随时迁移,客户端之间也能互通。这意味着你前期选错服务器配置、换机器、换系统盘,都只是运维成本的问题,而不是推倒重来的问题。Element 和 FluffyChat 一个是功能全面的重型客户端,一个是主打轻量的移动端客户端,它们都通过同一套账号体系和服务端通信,这一点在日常使用中非常舒服。
适合看这篇的人,我大概分三类:一是三五人小团队想把工作聊天记录收回到自己的服务器上;二是个人开发者想折腾一套长期可用的自托管聊天系统;三是纯粹对 Matrix(Element+FluffyChat) 这套技术栈感兴趣,想从部署入手理解整个架构的人。后面的内容我会尽量把为什么这么配置也讲明白,不只是丢给你一堆命令。
1. 为什么我最后选了 Matrix(Element+FluffyChat) 这套组合
1.1 Matrix 的三角关系:协议、服务端、客户端各管什么
通信系统最怕的就是一个软件把什么都干了,你根本没有选择余地。Matrix 把通信逻辑拆成三层:协议层负责定义消息如何路由和加密,服务端(也就是 Synapse)负责存储消息、管理房间和账号,客户端(Element、FluffyChat)负责把整个系统变成你能直接操作的界面。
你可以把 Matrix 协议理解成快递行业的标准箱体,Synapse 是某个快递仓库,Element 和 FluffyChat 则像是仓库开放的不同窗口。一个窗口功能齐全,另一个窗口更快更省事,但只要大家都按同一套箱体规格来,哪怕你的仓库和我朋友的仓库不是同一家建的,货物也能互通。这也是 Matrix 最核心的价值:服务端可以不同,客户端可以不同,但共同的语言是协议。
1.2 和通用商业群聊工具相比,自托管到底赢了什么
商业 IM 工具确实方便,注册就能用,但你会发现几个问题:成员数到一定量之后开始收费;历史消息导出要逐个手动;管理员没法真正限制某些成员的可见范围;更别提你想和其他系统打通时的各种限制。Matrix 自托管之后,这些痛点至少被绕开了一半。
成员上限由你自己的服务器配置决定,而不是授权数量;每一条消息都存在自己的数据库里,外面的人未经授权读不到;房间权限体系可以细致到一个用户能否改名、能否邀请、能否看历史消息。对我来说最有价值的一点是:服务器是我的,数据备份、迁移、恢复都按我的节奏来,而不是依赖某个第三方平台的政策。对一个需要保留长期记录的协作团队来说,这一点比任何花哨功能都重要。
1.3 为什么同时保留 Element 和 FluffyChat 两个客户端
先说结论:Element 适合当主力客户端,FluffyChat 适合手机端和低配置设备。Element 的桌面端和网页端几乎覆盖了 Matrix 协议里你能想到的所有功能,包括房间分组、置顶消息、空间管理、通知设置,以及可选的端到端加密。它的界面更接近现代通信软件的版型,第一次用不需要太多学习成本。
FluffyChat 则轻量得多,安装包体积小,启动速度快,在旧设备上也不容易发热。它同样能登录同一个 Synapse 服务器,能看到同样的房间和联系人,只是功能上做了减法。我实际分工是:电脑上挂 Element,手机上用 FluffyChat 收通知和简单回复,需要处理长消息或者复杂协作时再回到 Element。这种双客户端组合在同一个账号体系里无缝切换,体验上非常接近一个完整的协作系统。
2. 部署前规划:三个决定不做对,后面全是坑
2.1 先定 server_name,它基本没法改
Matrix 的 server_name 是识别你这个服务端的身份,一旦启用并被别的服务器记录,基本不能再改。server_name 我建议直接用你自己的域名,例如 chat.example.com。Synapse 在生成配置时会把 SYNAPSE_SERVER_NAME 写进 homeserver.yaml,之后所有注册账号、房间地址、联邦请求都会带上这个后缀。
如果你用了临时域名或者一个以后可能不要的域名,将来迁移成本非常高。我见过有人图省事用临时域名部署,跑了两个月想换域名,结果所有已创建账号、房间别名都要重新洗一遍,等于推倒重来。所以这个环节值得多花五分钟想清楚。
2.2 联邦开还是关:这是最容易忽略的架构决策
Matrix 默认支持联邦,也就是不同服务器之间可以互相建房间、互相私信。如果你的沟通系统只用在自己团队内部,不需要和外部世界互通,可以把联邦关掉或者做域名白名单限制,减少暴露面也更省流量。反过来,如果你希望和其他自建 Matrix 用户连接,那必须开放联邦所需的端口,或者通过 .well-known 让外部服务器找到你的统一入口。
我自己的做法是上线初期关闭联邦,等内部稳定后再打开。这样即使配置有问题,也不至于一上去就被各类扫描流量盯上。联邦开关不是我建议你直接抄的配置,而是一个需要你按真实需求做的决策:团队有没有外部协作对象?要不要和其他服务器的人语音通话?如果没有,关掉会比开着省心很多。
2.3 端口规划:8008、443、8448 到底怎么放
端口规划上,最常见的是 8008 给 Synapse 的 HTTP 服务,443 用于 HTTPS 统一入口,8448 用于联邦。如果你只有一台云服务器和一个域名,不需要为每个服务单独买公网 IP,也不需要完全对外暴露 8008。更推荐的做法是把所有流量统一收口到 443,再用 Nginx 这类工具做转发。
Element Web 如果和 Synapse 放在同一台机器上,也可以只占用一个内网端口,并不直接暴露公网。这样证书管理和防火墙规则都集中在同一层,排查问题不用到处翻。
| 组件 | 默认端口 | 公网是否暴露 | 说明 |
|---|---|---|---|
| Synapse 客户端 API | 8008 | 不需要 | 通过统一入口转发即可 |
| 联邦协议 | 8448 | 可选 | 想和其他服务器互联才需要开放 |
| Element Web | 8080 | 不需要 | 通过统一入口转发 |
| HTTPS 统一入口 | 443 | 必须 | 所有客户端访问的地址 |
3. Synapse 服务端安装:用 Docker Compose 从零跑起来
3.1 准备目录与编排文件
我的偏好是把服务端的配置目录放在一个固定目录下,所有数据都在这个目录里,方便备份也方便销毁重来。下面是一个最小可用的 Compose 文件:
services: synapse: image: matrixdotorg/synapse:latest container_name: matrix-synapse restart: unless-stopped volumes: - ./data:/data environment: SYNAPSE_SERVER_NAME: chat.example.com SYNAPSE_REPORT_STATS: "no" ports: - "127.0.0.1:8008:8008"这里我把端口映射到了本机回环地址,而不是 0.0.0.0。这样公网访问只能通过前面的 Nginx 统一入口进来,避免 Synapse 的 HTTP 服务直接暴露在公网。如果你只有内网环境,同样可以沿用这个写法。
3.2 生成配置并修改 homeserver.yaml
数据目录为空时,先运行一次生成命令,让 Synapse 自己生成 homeserver.yaml、日志配置和签名密钥:
docker compose run --rm synapse generate生成完成后,编辑 data/homeserver.yaml,重点确认这几项:
server_name: "chat.example.com" enable_registration: false listeners: - port: 8008 type: http tls: false bind_addresses: ["::"] x_forwarded: true resources: - names: [client, federation] compress: falsex_forwarded 必须打开,因为 Synapse 在 Nginx 后面,需要通过转发头拿到真实用户 IP 和请求协议。如果不开,后面做限速和日志分析时会发现所有请求都来自同一个内网地址,基本没法定位问题。
3.3 启动并验证服务状态
确认配置无误后启动:
docker compose up -d docker compose logs -f synapse日志最后出现类似“Listening on TCP port 8008”的信息,说明启动成功。再用一个请求验证客户端 API 是否可用:
curl http://127.0.0.1:8008/_matrix/client/versions如果返回一串 JSON,包含当前 Matrix 协议的版本信息,那服务端就活了。这一步一定要在本机先验证,再去做域名转发,不然等到了客户端排查阶段,你根本分不清是服务端没起来还是入口配置有问题。
4. 账号与管理员:先开门还是先上锁
4.1 第一步就用命令行创建管理员
在开放任何注册入口之前,先在服务器本地创建一个管理员账号:
docker compose exec synapse register_new_matrix_user -c /data/homeserver.yaml http://127.0.0.1:8008按提示输入用户名、密码,最后一项“Make admin [no]:”输入 yes。这里我强烈建议把管理员的日常账号和管理员账号分开:日常聊天用一个普通账号,管理员账号只用来做系统维护、创建房间、封禁用户。这样即使某天个人密码泄露,影响范围也小一些。
4.2 团队成员注册策略:宁可麻烦也不要开放注册
默认配置里 enable_registration 是 false,这并不代表不能加人,只代表客户端自助注册被关闭。你仍然可以通过命令行创建账号。如果团队有十几个人,我建议直接让管理员统一生成账号,再一对一发给成员。
为什么我不建议打开开放注册?因为 Matrix 服务端只要暴露在公网,就一定会被扫描器记录。哪怕你只开放半小时注册,也可能收到一批垃圾账号。最好的节奏是:新手阶段全部走命令行建号,等到你对这套系统有把握了,再考虑用注册口令或邮箱验证的方式自助开通。
# 如果需要临时开放注册,可以改这里 enable_registration: true registration_shared_secret: "填写一个足够长的随机字符串"用完之后立刻改回 false 并重启容器。这个 shared_secret 也用于构建注册令牌和管理接口,长度建议至少 32 位。
4.3 日常账号管理的小建议
账号建立之后,没有特别华丽的管理界面,但 Synapse 的管理 API 可以完成停用账号、重置密码、查看活跃会话等操作。我的日常习惯是给每个成员发账号时写清楚三条规则:第一,账号密码不要和其他内部系统共用;第二,加入房间时尽量用邀请链接;第三,成员离开后由管理员禁用账号,历史消息保留在房间而不是跟着人走。
这套规则配合 Matrix 的房间权限模型,基本可以满足小团队长期治理的需求。别小看这些约定,自建系统最怕的不是技术故障,而是权限意识没跟上,最后所有人都拿管理员账号到处跑。
5. Element Web 与 FluffyChat 的接入差异
5.1 Element Web 只需要一份 config.json
Element Web 是纯静态页面,部署方式非常简单。我习惯在同一个 Compose 文件里增加一个 element-web 服务,把配置文件挂进去:
element-web: image: vectorim/element-web:latest container_name: matrix-element restart: unless-stopped volumes: - ./element/config.json:/app/config.json:ro ports: - "127.0.0.1:8080:80"对应的 config.json 是客户端接入的关键:
{ "m.homeserver": { "base_url": "https://chat.example.com", "server_name": "chat.example.com" } }base_url 一定要指向你的统一入口,不要写成内网地址,也不要加 /_matrix 路径。Element 会自动在后面拼上标准的 API 路径。如果你用单独域名给 Element 做入口,那就把这个域名填进去。
5.2 FluffyChat 不需要部署,但登录地址要填对
FluffyChat 不是一个服务端组件,它只需要安装客户端,然后在登录界面选择自定义服务器,输入你的 Matrix 域名。很多人在这一步卡住,是因为填了全称,比如要填https://chat.example.com,并且不要带任何路径。
FluffyChat 对 HTTPS 证书的要求比较严格,如果你的证书链不完整,它会在登录时提示连接失败。这一点和网页端不同,浏览器有时会自动补全和容忍一些证书问题,移动端客户端通常更敏感。所以如果你用免费证书,一定确保证书链完整,最好在浏览器里用无痕模式手动访问一遍再让成员去填。
5.3 单域名统一入口的 Nginx 转发配置
如果你和我一样想让 Element Web 和 Matrix API 共用同一个域名,可以用类似下面的配置。这个方案只需要一条 DNS 记录,证书管理也最简单。注意这里只列出关键位置片段:
server { listen 443 ssl; server_name chat.example.com; location /.well-known/matrix/ { proxy_pass http://127.0.0.1:8008; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; } location /_matrix { proxy_pass http://127.0.0.1:8008; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; } location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; } }这套配置的关键是把/.well-known/matrix/和/_matrix都转给 Synapse,其余页面转给 Element Web。这样用户只需要记住一个网址,Element 和 FluffyChat 也都能自动发现服务器。
6. 安全加固与隐私边界:上线前别省这几步
6.1 限速、上传大小与基础防护
Synapse 默认配置对单机小团队来说足够,但如果你暴露在公网,还是建议手动加上基础防护。进入 homeserver.yaml,找到或者新增以下配置:
max_upload_size: 20M rc_messages_per_second: 0.2 rc_message_burst_count: 10 rc_registration: per_second: 0.17 burst_count: 3这些参数不是越高越好。上传大小限制到 20M 能避免有人把文件服务器当网盘用;消息限速稍微收紧一些,能减少被滥用时的风险。如果团队内部确实需要传大文件,我更推荐结合文件分享工具而不是直接把上传限制放得很大。
6.2 联邦白名单与访问边界
如果你的联邦需求是只和某几台可信服务器互通,而不是全球任何 Matrix 服务器都能连进来,可以用白名单约束:
federation_domain_whitelist: - your-team-partner.example.com - another-trusted.example.net加了白名单之后,Synapse 只接受和这些域名之间的联邦请求。这样即使有人扫描到你的存在,也没法用攻击性节点填满你的房间。
6.3 媒体存储:最容易爆盘的地方
Matrix 房间里的图片、视频、文件都存放在服务端的 media_store 目录。小团队聊几天可能感觉不到,但时间长了,这个目录的膨胀速度会超过数据库。我见过不少自建实例最后挂掉的原因不是进程崩溃,而是磁盘被媒体文件占满。
所以上线第一天就应该规划好 media_store 的位置,最好单独挂一块数据盘或者单独的卷。日常维护里,也要定期查看这个目录的体积,有历史会话需求的使用归档策略,不需要的可以清理旧的媒体缓存。Synapse 本身就提供定时清理任务相关的配置项,但小团队一般手动检查就够了。
7. 三个隐蔽问题的完整排查复盘
7.1 问题一:Element 网页一直空转,登录按钮点了没反应
我最初把 Element Web 部署好之后,页面能打开,但输入账号密码后一直转圈,控制台里能看到一条请求失败记录。第一反应是服务端没起来,但 curl 验证 8008 端口是正常的,说明 Synapse 没问题。
接着我用浏览器开发者工具去查看具体失败请求,发现客户端访问的是https://chat.example.com/.well-known/matrix/client,而这个路径返回了 404。原因很简单:我只在 Nginx 里配置了/_matrix的转发,没有把/.well-known/matrix/转给 Synapse。Element 启动时会先去这个地址做服务器发现,发现不了就直接放弃。
排查链路就是四条:先看服务端日志,然后看网关转发日志,再用浏览器开发者工具定位具体 URL,最后补上 well-known 路由。补完之后刷新页面,登录恢复正常。
7.2 问题二:FluffyChat 提示连不上服务器,但网页端是好的
FluffyChat 登录时选择自定义服务器,输入域名后提示连接失败。这个问题最迷惑的地方在于同一台服务器的网页端能正常登录,所以服务端和网关都没问题。
我一层层往下查,先是直接浏览器访问https://chat.example.com/.well-known/matrix/client,正常返回 JSON。然后我用手机的移动网络去访问,还是正常。最后发现问题出在手机上的 DNS 缓存和证书链两个因素叠加。FluffyChat 在请求时对证书链校验比浏览器更严格,我的中间证书在 Nginx 里漏配了一段,导致移动端在特定网络环境下验证失败。
解决方法是把完整的证书链文件重新配置到 Nginx,重启后再在 FluffyChat 里清除账号缓存重新登录。这类问题最能说明一个道理:网页端能打开不代表移动端能连上,证书链完整性和 DNS 解析一定要在真机上验证。
7.3 问题三:升级一次服务端之后,联邦突然失效
有一次我把 Synapse 镜像升级到新版本,内部聊天一切正常,但和另一台服务器的联邦房间无法收发消息。日志里反复出现握手失败,外部服务器找不到我的 server_name。
排查第一步是确认联邦发现文件是否正常,我在服务器上执行请求验证.well-known/matrix/server返回了{"m.server":"chat.example.com:443"},看起来是对的。第二步检查外部服务器是不是在通过 8448 端口连接,发现对方发起的请求被防火墙拦住了。原因是我升级前把联邦端口从 8448 改成了通过 443 走,但外部服务器还在按旧地址请求 8448。
最终我选择把 8448 也配置到 Nginx 统一入口里,保证新旧两条联邦发现路径都能走通。这个过程提醒我:升级之前一定要完整记录当前的网络和 DNS 配置,不要只看服务端版本号。
8. 日常维护:备份、升级、日志清理
8.1 数据库备份:最容易被忽略的一步
Synapse 默认使用 SQLite 数据库,小团队场景下完全够用。备份 SQLite 不要直接复制数据库文件,因为写入过程中直接拷贝可能得到损坏的结果。安全做法是通过 sqlite3 的在线备份命令导出:
docker compose exec synapse sqlite3 /data/homeserver.db ".backup '/data/homeserver_backup.db'" docker compose cp synapse:/data/homeserver_backup.db ./backups/如果团队规模继续增长,建议切换到 PostgreSQL。切换过程并不复杂,但要先把历史数据迁移过去。我个人的判断标准是:如果一天活跃用户超过 30 人,或者单个房间成员超过 50 人,就值得从 SQLite 升级到 PostgreSQL。
数据库之外,还要备份 media_store 目录和 homeserver.yaml。签名密钥尤其重要,如果丢失,之前的所有会话签名验证都会失效。整个数据目录里,我个人会把data/keys/单独记进备份清单,防止只备份数据库的错觉。
8.2 升级顺序与回滚策略
每次升级前先备份,再拉取新镜像:
docker compose pull synapse docker compose up -d docker compose logs -f synapse升级后重点看两件事:数据库自动迁移是否成功,以及客户端是否出现新的兼容性告警。Matrix 生态更新节奏不慢,有时候客户端升级和服务端升级并不同步,会出现网页端正常但移动端异常的情况。我的习惯是先把服务端稳定一个版本,再提醒团队成员更新客户端,而不是两方面同时滚动。
如果真的升级失败,不要慌。把 Compose 文件里的镜像版本退回去,用备份数据恢复,然后等待问题明确后再做下一次尝试。小团队自建系统最忌讳的就是追新,稳定比功能多重要得多。
8.3 日志与媒体目录的日常检查
平时不需要时刻盯着日志,但遇到异常时最常用的就是:
docker compose logs --tail 100 -f synapse磁盘空间是我每周唯一会固定检查的指标。看媒体目录大小,看数据库备份文件是否按时生成。有时候成员会往房间里发大视频,一次就能吃掉几个 G。如果空间预警,我会优先清理媒体缓存而不是收紧上传尺寸,减少对正常使用的干扰。
这套系统在我这里跑到现在,最大的收获不是省下多少订阅费,而是让团队每个人都知道信息到底存在哪里、由谁管理。如果你也准备上手,我建议从最小配置开始,先让五个成员用起来,跑通一天后再慢慢加联邦、加客户端、加备份策略。稳定的基础不是一次把所有功能配齐,而是把最小闭环走顺之后再逐步扩展。