用 Docker Compose 自托管 Rocket.Chat
在 VPS 上用 Docker Compose 自托管 Rocket.Chat:它必需的单节点 MongoDB 副本集、TLS、备份,以及每一种故障的修复方法。
您将搭建什么
一个完全归您所有的私有团队聊天平台:Rocket.Chat 在您自己的 VPS 上以 Docker Compose 运行,由 TLS 终止加密,每一条消息都存放在一个您可以备份和迁移的 MongoDB 数据库中。Rocket.Chat 是成熟的开源 Slack 和 Teams 替代品,提供频道、私信、话题串、文件共享以及语音和视频,全部运行在您租用并掌控的硬件上。应用本身只是一个容器,几分钟就能启动。真正会出问题的地方在于它旁边的数据库,所以本指南大部分内容都在讲 MongoDB,尤其是那个每个人第一次都会意外的要求:Rocket.Chat 无法在独立部署(standalone)的 MongoDB 上运行。它需要一个副本集,哪怕这个"集"只有一个节点。
前提条件,以及没人告诉您的内存账
老实地评估服务器的规格。对一个小团队而言,现实的最低配置是 2 个 vCPU 和 4 GB 内存。Rocket.Chat 的 Node.js 进程本身大约需要 1 到 1.5 GB,而 MongoDB 的 WiredTiger 缓存默认会占用剩余内存的大约一半。在一台 2 GB 的 VPS 上,两者在启动时勉强放得下,但真实流量一到就会相互挤压:MongoDB 扩大缓存,Node 扩大堆内存,内核耗尽可用页,内存不足终止程序(out-of-memory killer)就会杀掉当前占用最大的进程,通常是 mongod。容器打印出 Killed,Docker 重启它,于是您得到一个在本该轻松应付的负载下每隔几分钟就掉线的聊天服务器。2 GB 适合两个人随便试用,但不足以当团队服务器。起步就用 4 GB;如果您预计会有几十个并发用户、视频通话,或者不断增长的上传记录,就给它 8 GB。
开始之前,您还需要准备好三样东西。一个域名,其 A 记录指向 VPS 的公网 IP,Rocket.Chat 的实时功能和移动客户端需要一个稳定的主机名,而不是裸 IP。在服务器防火墙和您服务商的网络防火墙上都开放 80 和 443 端口,后者在大多数控制面板上是一个独立的设置。以及一台全新的 Ubuntu 24.04 KVM VPS,拥有 root 或 sudo 权限。如果您还在考虑聊天服务器是否是值得先跑起来的第一个服务,2026 年有哪些值得自托管的服务这篇指南梳理了其中的取舍。
安装 Docker 引擎和 Compose 插件
使用 Docker 官方的 apt 仓库,而不是 Ubuntu 自带的 docker.io 包,也不是那个陈旧的独立 docker-compose Python 二进制程序。现代的 Compose 是一个 Docker 插件,调用方式是 docker compose,中间是空格,不是连字符。老的 docker-compose v1 已经停止维护,无法正确处理下面用到的健康检查和依赖语法。
sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin确认两个组件都已就位:
sudo docker version
sudo docker compose versiondocker compose version 打印出类似 Docker Compose version v2.x 的内容,这才是要看的检查点。如果它报错 docker: 'compose' is not a docker command,说明插件没装上,稍后您会遇到令人困惑的故障,请在这里就修好它。
compose 文件:把 MongoDB 作为单节点副本集
这是人们最容易搞错的部分,请慢慢读。Rocket.Chat 使用 MongoDB 的变更流(change streams)把新消息实时推送给已连接的客户端,而变更流只在副本集上可用。把 Rocket.Chat 指向一个普通的独立 mongod,它会连上,却打不开变更流,然后无限重启循环。解决办法并不复杂:您运行单个普通的 MongoDB 容器,但用 --replSet 启动它,然后初始化一个只有一个成员的副本集。
创建一个工作目录和一个 compose.yml:
services:
mongodb:
image: mongo:8.0
restart: always
command: ["mongod", "--replSet", "rs0", "--bind_ip_all", "--oplogSize", "128"]
volumes:
- mongodb_data:/data/db
- mongodb_config:/data/configdb
healthcheck:
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping')"]
interval: 10s
timeout: 10s
retries: 12
rocketchat:
image: registry.rocket.chat/rocketchat/rocket.chat:8.5.1
restart: always
depends_on:
mongodb:
condition: service_healthy
environment:
MONGO_URL: "mongodb://mongodb:27017/rocketchat?replicaSet=rs0"
MONGO_OPLOG_URL: "mongodb://mongodb:27017/local?replicaSet=rs0"
ROOT_URL: "https://chat.example.com"
PORT: "3000"
ports:
- "127.0.0.1:3000:3000"
volumes:
mongodb_data:
mongodb_config:这里有几处是刻意为之的选择。Rocket.Chat 的端口发布到 127.0.0.1:3000,而不是 0.0.0.0,应用本身没有 TLS,所以只应让同一台机器上的反向代理能访问它;把它绑定到所有网络接口,等于把一个明文登录页直接暴露在公网上。MongoDB 则完全不对宿主机发布;它只能通过 Compose 的内部网络以 mongodb 这个名字访问,而这正是 MONGO_URL 使用的主机名。MONGO_URL 带有 ?replicaSet=rs0,去掉它,驱动就会把服务器当作独立部署,哪怕它其实是副本集,变更流照样会失败。MONGO_OPLOG_URL 指向存放 oplog 的 local 数据库;现代 Rocket.Chat 更倾向于用变更流,但设置它没有坏处,还能让较旧的代码路径正常工作。depends_on 使用了 condition: service_healthy,所以 Compose 会一直等到 MongoDB 能响应 ping 之后,才启动 Rocket.Chat,这正是健康检查的用途。
给两个镜像都固定明确的版本标签,这里是 mongo:8.0 和一个明确的 Rocket.Chat 版本如 8.5.1,永远不要用 :latest,它会把一次无人值守的 docker pull 变成一次意外且无法迁移的升级。在固定之前,先查一下当前稳定的 Rocket.Chat 版本以及它支持的 MongoDB 版本。Rocket.Chat 为每个版本发布一份机器可读的信息文档:curl -s https://releases.rocket.chat/8.5.1/info | jq '{compatibleMongoVersions, lts}' 对 8.5.1 会返回 compatibleMongoVersions: ["8.0"],所以 mongo:8.0 是唯一受支持的引擎;此外还有一个 lts 标志,告诉您该版本是否是长期支持(LTS)构建,值得为一台您不想天天照看的服务器固定使用。
初始化副本集
把这套服务启动起来:
sudo docker compose up -dRocket.Chat 会立刻开始崩溃,Docker 会不断重启它,这是预期之中的,因为副本集还不存在。手动创建它一次:
sudo docker compose exec mongodb mongosh --eval 'rs.initiate({_id: "rs0", members: [{_id: 0, host: "mongodb:27017"}]})'正确的结果是 { ok: 1 }。几秒钟内,这个单一节点会把自己选为主节点(primary);用下面的命令确认:
sudo docker compose exec mongodb mongosh --quiet --eval 'rs.status().members[0].stateStr'您希望看到 PRIMARY。整篇文章里最重要的一个细节就是 host: "mongodb:27017" 这个参数。如果您运行不带成员列表的裸 rs.initiate(),MongoDB 就会用容器的内部主机名来对外通告副本集,那是一串随机哈希,比如 a1b2c3d4e5f6。Rocket.Chat 从自己的容器里连接,无法解析那个名字,于是 MongoDB 驱动对它的 DNS 解析失败,一直循环并记录 MongoServerSelectionError: getaddrinfo ENOTFOUND a1b2c3d4e5f6。始终用与 MONGO_URL 匹配的明确服务名来初始化。
首次启动:盯着它跑起来
一旦副本集成为主节点,Rocket.Chat 的下一次重启就能干净地连上,并开始首次运行的数据迁移。跟踪日志:
sudo docker compose logs -f rocketchat您要等的是启动横幅:
+--------------------------------------------+
SERVER RUNNING
Rocket.Chat Version: 8.5.1
NodeJS Version: 22.22.3 - x64
+--------------------------------------------+首次启动很慢,应用要运行数据库迁移并建立索引,所以在担心之前先给它一两分钟。如果日志反而反复出现 MongoServerSelectionError: Server selection timed out after 30000 ms,并带有类型为 ReplicaSetNoPrimary 的拓扑描述,说明副本集没有初始化;如果它反复出现针对某个随机哈希的 getaddrinfo ENOTFOUND,说明初始化时用错了主机名。无论哪种情况,都退回上一步。一旦看到 SERVER RUNNING,Rocket.Chat 就在 127.0.0.1:3000 上监听了,该在它前面加上真实的主机名和 TLS 了。
给它加上 TLS
永远不要用纯 HTTP 暴露 Rocket.Chat。只要用 http:// 登录一次,您就把管理员密码交给了链路上的任何人。在同一台机器上的反向代理里终止 TLS,然后转发到 127.0.0.1:3000。有两件事很关键:代理必须转发 WebSocket 升级头,因为 Rocket.Chat 是实时的,缺了它就会出问题;而且容器的 ROOT_URL 必须与用户输入的公开 HTTPS 地址完全一致。
先从一个纯 HTTP 的 nginx server 块开始,它代理到应用并转发升级头。把它保存为 /etc/nginx/sites-available/rocketchat,在 sites-enabled 里建立符号链接,然后重新加载:
server {
listen 80;
server_name chat.example.com;
client_max_body_size 100M;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}暂时先让它留在 80 端口,一个带 listen 443 ssl; 却没有证书的块甚至通不过 sudo nginx -t。重新加载 nginx(sudo nginx -t && sudo systemctl reload nginx),然后签发证书。在 Ubuntu 上最简洁的做法是用 Certbot 和 nginx 申请 Let's Encrypt TLS 证书:certbot --nginx 会就地改写上面的块,加上 listen 443 ssl;、ssl_certificate 各行,以及一个自动的 80 到 443 跳转,还会为您安排续期。如果您已经在一个代理后面运行了好几个容器,用 Traefik 为多个 Docker 应用自动配置 TLS是更整洁的选择,只需给 rocketchat 服务加上 router 和 service 标签,Traefik 就会为您申请并续期证书,完全不需要 nginx 块。无论哪种方式,都在 compose.yml 里把 ROOT_URL 设为 https://chat.example.com,再重新运行 sudo docker compose up -d,让容器读取到这个改动。如果您希望服务器只在您自己的网络内部可访问,而不是暴露在公网上,就在它前面架一个在 VPS 上自托管的 WireGuard VPN,并把代理绑定到隧道地址。
首次运行的安装向导
访问 https://chat.example.com,Rocket.Chat 会带您走完一个简短的向导。首先是管理员账户,填真实姓名、用户名、邮箱和一个强密码;这是目前唯一存在的账户,所以别弄丢它。接着是组织和服务器信息,名称、行业、规模、站点名称和默认语言;这些只是表面设置,填一下就往下走。然后是真正重要的选择:把这个工作区注册到 Rocket.Chat Cloud,还是保持独立(standalone)。
注册会通过 Rocket.Chat 的网关启用移动推送通知和插件市场,代价是与 Rocket.Chat 的云建立一层控制面关系。独立模式让服务器完全私有、不依赖外部,但 iOS 和 Android 的推送通知会失效,因为苹果和谷歌不允许自行构建的应用持有推送证书,官方应用是通过云网关来路由的。如果隐私就是全部意义、而且您的用户主要用网页版,就选独立;如果移动推送不可或缺,就选注册。之后您可以在管理后台里改变主意。
在邀请任何人之前先锁紧它
Rocket.Chat 出厂时开放注册是打开的,默认情况下注册表单(Registration Form)设为 Public,所以任何找到这个网址的人都能创建账户。在一个公开的主机名上,这等于一扇敞开的门。进入 Admin → Settings → Accounts → Registration,把 Registration Form 设为 Disabled,这样您就通过手动或邀请链接来创建账户,或者设为 Secret URL。顺便,把 Allow Anonymous Read 和 Allow Anonymous Write 关掉,除非您确实想要一个公开的只读频道。
也要决定上传文件存到哪里。默认的文件上传(File Upload)存储是 GridFS,它把每一张图片和附件都存进 MongoDB 本身。这很简单,但也意味着随着大家不断粘贴截图,您的数据库以及您每次做的 mongodump 都会无止境地增大。在 Admin → Settings → File Upload 里,您可以把存储切换到本地文件系统或兼容 S3 的存储桶,并设置一个合理的文件大小上限。对小团队来说 GridFS 没问题,只要知道备份会随时间变得越来越大就行。
用 mongodump 做备份
您所有的数据都存放在 mongodb_data 卷里。不要在数据库运行时直接把卷复制出来,要用 mongodump 做一份一致性的转储,并把它流式写入宿主机上的一个文件:
sudo docker compose exec -T mongodb mongodump --db rocketchat --archive --gzip > rocketchat-$(date +%F).archive.gz那个单独的 gzip 归档就是您的整个工作区:用户、频道、消息、设置,以及,如果您把上传保留在 GridFS 上,还有那些文件。如果您把上传移到了文件系统或 S3,就要单独备份那个存储。恢复到一套全新的服务时,先初始化副本集,然后:
sudo docker compose exec -T mongodb mongorestore --archive --gzip --drop < rocketchat-2026-07-15.archive.gz把归档复制到这台机器之外,对象存储、另一台服务器,任何一个在 VPS 挂掉时不会把备份一起带走的地方,并用 cron 每晚运行转储。一份从未恢复过的备份只是一种指望,而不是备份;在一台用完即弃的 VPS 上练习一次恢复,好让您在真正需要之前就确认它管用。
升级:固定标签、读发布说明、遵守 Mongo 版本矩阵
两条规则能让升级平淡无奇。第一,一次只升级 Rocket.Chat 一个大版本。它在启动时运行数据结构迁移,并且故意拒绝跨大版本跳跃;试图从 6.x 直接升到 8.x,它会以一个迁移错误停下来,而不是破坏您的数据。把镜像标签提升到下一个大版本的最新版,阅读那个版本的发布说明里的破坏性改动,运行 docker compose up -d,并在继续之前盯着日志把迁移跑完。第二,遵守 MongoDB 的支持矩阵。每个 Rocket.Chat 版本支持一组特定的 MongoDB 版本,curl -s https://releases.rocket.chat/<version>/info | jq .compatibleMongoVersions 会告诉您是哪些。当您确实要升级 MongoDB 时,比如从 7.0 到 8.0,一次只跨一个大版本,并在每一跳之后设置功能兼容版本(feature compatibility version)。在 MongoDB 8.0 上,这条命令需要显式的 confirm: true,否则它会拒绝执行,并提示您带上确认标志重新运行:
sudo docker compose exec mongodb mongosh --eval 'db.adminCommand({setFeatureCompatibilityVersion: "8.0", confirm: true})'在升级任何一个组件之前都先做一次 mongodump。这就是全部的保险。
故障模式,附带确切的报错字符串
Rocket.Chat 在 docker compose up 之后立刻陷入重启循环,而 docker compose logs rocketchat 里全是 MongoServerSelectionError。 MongoDB 在运行,但驱动选不出主节点,而确切的报错字符串会告诉您犯了哪个错。Server selection timed out after 30000 ms 加上拓扑类型 ReplicaSetNoPrimary,意味着您从没运行过 rs.initiate(),副本集还没有配置。getaddrinfo ENOTFOUND 后面跟着一串随机哈希,意味着您初始化时没有指定明确的 host: "mongodb:27017",所以 MongoDB 通告了一个无法解析的容器主机名。用 sudo docker compose exec mongodb mongosh --eval 'rs.status()' 来诊断:如果它报错 MongoServerError: no replset config has been received,就初始化副本集;如果它显示某个成员的 name 是一串随机哈希,就用服务名重新初始化。
网页界面能加载,但登录一直转圈,永远完不成。 打开浏览器控制台,您会看到 WebSocket connection to 'wss://chat.example.com/websocket' failed。这几乎总是 ROOT_URL 不匹配,或者代理没有转发升级头。确认 ROOT_URL 等于包含 https:// 在内的确切公开地址,并且您的 nginx location 块设置了 Upgrade 和 Connection "upgrade",还带上 proxy_http_version 1.1。改动其中任一项后,重新运行 docker compose up -d。
某个容器不断死掉,docker compose ps 显示它处于 Restarting。 docker compose logs 在某一行中间就断了,而 sudo dmesg | tail 显示来自 oom-killer 的 Out of memory: Killed process 12345 (mongod);退出码是 137。这台机器内存耗尽了。真正的解决办法是换一台更大的 VPS,至少 4 GB。作为权宜之计,可以加上交换分区(swap),并在 MongoDB 的 command 里用 --wiredTigerCacheSizeGB 1 限制它的缓存,但在真实负载下 swap 只是推迟下一次 OOM:
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfiledocker compose up 失败,报出 Error response from daemon: driver failed programming external connectivity ... bind: address already in use。 已经有别的东西占用了 3000 端口,通常是一个没有干净停止的旧 Rocket.Chat 容器,或者另一个应用。用 sudo ss -ltnp | grep :3000 找到它,停掉那个进程或容器,或者把端口映射的宿主机一侧改成 127.0.0.1:3001:3000,并相应更新代理的 proxy_pass。
FAQ
Rocket.Chat 真的需要 MongoDB 副本集吗?
是的,即便是只有一个数据库节点的单台服务器也需要。Rocket.Chat 使用 MongoDB 变更流来实时投递消息,而变更流是副本集专属的功能,独立的 mongod 打不开它。您不需要多台机器;只需运行一个用 --replSet rs0 启动的 MongoDB 容器,并用 rs.initiate() 初始化一个单成员副本集。跳过这一步,驱动就永远找不到主节点,于是 Rocket.Chat 会带着 MongoServerSelectionError: Server selection timed out 陷入重启循环,永远启动不完。
自托管的 Rocket.Chat 需要多少内存?
把 4 GB 作为现实的最低配置来规划,繁忙的团队则规划 8 GB。Rocket.Chat 的 Node 进程大约占用 1 到 1.5 GB,而 MongoDB 会拿走剩余内存的大约一半给它的 WiredTiger 缓存,所以在一台 2 GB 的机器上两者会相互挤压,只要有任何真实负载,内存不足终止程序就会杀掉 mongod,日志里显示 Killed,退出码为 137。2 GB 只够用几个测试用户来评估这个软件。
我该怎么给 Rocket.Chat 加上 HTTPS?
在同一台 VPS 上运行一个反向代理,由它终止 TLS 并转发到 127.0.0.1:3000,并把容器的 ROOT_URL 设为您公开的 https:// 地址。代理必须转发 WebSocket 升级头,否则登录会卡住。对单个应用来说,Certbot 配 nginx 是最简单的方案;如果您在一个代理后面运行多个容器并想要自动的证书管理,Traefik 更整洁。
我该怎么备份自托管的 Rocket.Chat?
用 mongodump 做一份一致性的数据库转储,而不是复制卷:docker compose exec -T mongodb mongodump --db rocketchat --archive --gzip > backup.archive.gz。那个归档包含用户、频道、消息和设置,如果您把存储保留在 GridFS 上,还包含上传的文件。把它复制到服务器之外,用 cron 每晚自动执行,并在一台用完即弃的机器上演练一次 mongorestore,好让您确认恢复确实管用。
我该怎么升级 Rocket.Chat 而不搞坏 MongoDB?
一次只升级 Rocket.Chat 一个大版本,它在启动时运行迁移并拒绝跳过大版本,在提升固定的镜像标签之前先阅读每个版本的发布说明。用 curl -s https://releases.rocket.chat/<version>/info | jq .compatibleMongoVersions 查看您的目标版本支持哪些 MongoDB 版本,升级 MongoDB 时一次只跨一个大版本,并在每一跳之后带着 confirm: true 设置 setFeatureCompatibilityVersion。永远先做一次 mongodump。