SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor

使用 Docker Compose 自建 Rocket.Chat 完整指南

本指南介绍如何通过 Docker Compose 在 VPS 上部署 Rocket.Chat。重点解决 MongoDB 副本集配置要求、内存计算陷阱以及 OOM Killer 导致容器频繁重启的故障排查方案,确保私有聊天服务器稳定运行。

您将构建的内容

一个完全由您掌控的私有团队聊天系统:Rocket.Chat 运行在您自己的 VPS 上,由 Docker Compose 管理,通过 TLS 终止连接,所有消息都存储在 MongoDB 数据库中,您可以对其进行备份和迁移。Rocket.Chat 是成熟的开源 Slack 和 Teams 替代方案,提供频道、直接消息、线程、文件共享以及语音和视频功能,全部运行在您租用并控制的硬件上。不过,它并不是唯一可靠的选项。如果您仍在选择,Mattermost、Rocket.Chat、Synapse 和 Zulip 对比会从后续容易出问题的方面进行比较:RAM、数据库、移动推送、SSO 和升级。应用本身是一个容器,几分钟内即可启动。实际导致故障的内容都在旁边的数据库中,因此本指南大部分内容都围绕 MongoDB,尤其是第一个会让所有人首次遇到时感到意外的要求:Rocket.Chat 无法连接到独立运行的 MongoDB。它需要副本集,即使这个“集合”只有一个节点。

前置条件,以及没人告诉你的 RAM 计算

请如实评估服务器规格。小型团队的现实最低配置是 2 vCPU 和 4 GB RAM。Rocket.Chat 的 Node.js 进程本身大约需要 1 到 1.5 GB;MongoDB 的 WiredTiger 缓存默认取以下两者中较大的值:总 RAM 减去 1 GB 后的 50%,或 0.256 GB。这个默认值假设 mongod 独占整台机器:它根据总 RAM 计算,不考虑 Node 已经占用的内存。因此,在 4 GB 服务器上,缓存会达到 1.5 GB;在 2 GB 服务器上,约为 0.5 GB。这些内存还不包括 Node 的堆、mongod 在缓存之外使用的内存,以及操作系统本身所需的内存。在 2 GB VPS 上,这两个组件启动时可以勉强运行,但真实流量一到就会发生内存竞争:MongoDB 增长缓存,Node 增长堆,内核耗尽可用页面,out-of-memory killer 会终止占用内存最多的进程,通常是 mongod。容器输出 Killed,Docker 随后重启容器。结果是,聊天服务器在本应轻松应对的负载下每隔几分钟就中断。2 GB 适合两个人试用,但不适合作为团队服务器。建议从 4 GB 开始;如果预计有几十个并发用户、视频通话或不断增长的上传历史记录,请使用 8 GB。还要为服务器承载的其他服务预留资源:在同一台 VPS 上部署 类似 Notion 的 AFFiNE 工作区,会增加 4 个容器并争用相同的内存页面,因此这些容器所需的 RAM 应额外计算,不能从 Rocket.Chat 的内存预算中扣除。

开始前还需要准备 3 项内容。首先是一个 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 compose 调用的 Docker 插件,中间使用空格,而不是连字符。旧版 docker-compose v1 已经停止维护,无法正确处理下面的 healthcheck 和依赖语法。

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 version

docker compose version 输出版本行才是需要检查的内容。当前版本会输出 Docker Compose version v5.x(截至 2026-10-10,最新版本是 2026 年 10 月 2 日发布的 v5.6.0);较旧的安装仍可能输出 v2.x,Docker 同时支持这两个版本。Docker 说明 Compose v5 与 v2 在功能上完全相同,只是将版本号设为 v5,以避免与旧版 v2 和 v3 Compose 文件格式混淆,因此本指南使用任一版本都可以。如果出现 docker: 'compose' is not a docker command,说明插件未安装,后续操作将出现难以理解的错误。请在此处修复。

Compose 文件:MongoDB 作为单节点副本集

这部分最容易出错,请仔细阅读。Rocket.Chat 使用 MongoDB 的变更流实时向已连接的客户端推送新消息,而变更流只在副本集中可用。将 Rocket.Chat 指向普通的独立 mongod 后,它会连接 MongoDB,但无法打开变更流,随后无限重启。解决方法并不复杂:运行一个普通的 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,用于显式指定副本集名称;MongoDB 建议设置该参数,以确保不同驱动程序的行为一致。不设置它本身不会导致故障:directConnection 默认为 false,因此只要初始化副本集时使用了 Rocket.Chat 可以解析的主机名,驱动程序仍会发现副本集并将操作发送到主节点。下一节会完成这项配置。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 会为每个版本发布机器可读的信息文档:对于 8.5.1,curl -s https://releases.rocket.chat/8.5.1/info | jq '{compatibleMongoVersions, lts}' 会返回 compatibleMongoVersions: ["8.0"],因此 mongo:8.0 是唯一受支持的引擎;此外还有一个 lts 标志,用于说明该版本是否为长期支持版本。如果服务器不希望频繁维护,应优先固定这类版本。并非所有项目都会发布带版本号的镜像;在这种情况下,应固定源代码版本:自行托管 openGym 健身追踪器意味着检出特定的 git 标签并从该标签构建,而不是跟随不断变化的分支。

初始化副本集

启动整个堆栈:

sudo docker compose up -d

Rocket.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 匹配的显式服务名进行初始化。

首次启动:监控启动过程

设置为 primary 后,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 后

切勿让 Rocket.Chat 直接通过 HTTP 暴露。通过 http:// 登录一次,就等于将管理员密码交给链路上的任何人。在同一台服务器上的反向代理中终止 TLS,再将请求转发到 127.0.0.1:3000。有两点需要注意:代理必须转发 WebSocket 升级请求头,因为 Rocket.Chat 依赖实时通信,缺少这些请求头就会失效;容器中的 ROOT_URL 必须与用户输入的公网 HTTPS 地址完全一致。

先创建一个普通的 HTTP nginx server block,将请求代理到应用,并转发升级请求头。将其保存为 /etc/nginx/sites-available/rocketchat,再创建符号链接到 sites-enabled,然后重新加载 nginx:

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 的规则,并为你安排证书续期。如果你已经通过一个代理运行多个容器,为多个 Docker 应用配置自动 TLS 的 Traefik 更简洁。只需将路由器和服务标签添加到 rocketchat 服务,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,或将其保持为独立模式。

注册后,您可以通过 Rocket.Chat 的网关启用移动推送通知,并使用附加组件市场,但代价是需要与 Rocket.Chat 云服务建立控制平面关联。通过该网关发送的推送通知也会计入工作区的月度限额。独立模式可让服务器完全私有,且不依赖外部服务;但官方 iOS 和 Android 应用将无法再接收推送通知,因为 Rocket.Chat 文档说明这些应用只能使用其云网关,而该网关不会为未注册的工作区提供服务。解决方法是自行构建移动应用的白标版本:在 Push 设置中关闭网关,填写您自己的 APNs 证书和 Firebase Cloud Messaging 凭据,然后让用户安装您的应用,而不是官方应用。如果隐私是主要目标,且用户主要使用 Web 应用,或您准备自行发布移动应用,请选择独立模式。如果官方应用必须支持推送通知,请选择注册。以后可以在 Admin 中更改此设置。

在邀请任何人之前先收紧配置

Rocket.Chat 默认开启开放注册,Registration Form 默认设置为 Public,因此任何找到 URL 的人都可以创建帐户。在公网主机名上,这相当于门户大开。转到管理 → 设置 → 帐户 → 注册,将 Registration Form 设置为 Disabled,这样就可以手动创建帐户或通过邀请链接创建帐户;也可以设置为 Secret URL。在此页面中,同时关闭 Allow Anonymous Read 和 Allow Anonymous Write,除非您确实需要公开的只读频道。如果手动创建每个帐户过于繁琐,并且这不是团队成员登录的唯一服务,请将 Rocket.Chat 的 OAuth 登录指向自托管的 Authentik SSO 服务器。这样可以在一个位置统一处理成员加入和离开,而不必在每个应用中分别操作。

同时确定上传文件的存储位置。默认的 File Upload 存储方式是 GridFS,它会将每张图片和每个附件直接存储在 MongoDB 中。这样配置简单,但意味着随着用户不断粘贴屏幕截图,数据库以及您创建的每个 mongodump 都会持续增长。转到管理 → 设置 → 文件上传,可以将存储方式切换为本地文件系统或兼容 S3 的存储桶,并设置合理的最大文件大小。如果团队交换的是完整的照片库,而不是偶尔发送屏幕截图,应将这些文件存储在专用照片服务器中,而不是聊天数据库中;PhotoPrism 与 Immich 对比会比较两者的内存开销和所需的备份命令。对于小型团队,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 上练习恢复,确认流程可用后再依赖它应对故障。

升级:固定标签、阅读说明、遵循 MongoDB 兼容矩阵

有两条规则可以让升级过程保持可控。第一,每次只将 Rocket.Chat 升级一个主版本。 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 正在运行,但驱动程序无法选择 primary;精确字符串可以指出具体配置错误。拓扑类型为 ReplicaSetNoPrimary 的 Server selection timed out after 30000 ms 表示你从未运行 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 是随机哈希值,请使用服务名重新初始化。

Web UI 可以加载,但登录一直转圈且无法完成。 打开浏览器控制台,你会看到 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 限制缓存大小。该值应低于默认值,而不是高于默认值。例如,4 GB 服务器可使用 1(默认值为 1.5),2 GB 服务器可使用 0.3(默认值约为 0.5)。但在实际负载下,swap 只能延迟下一次 OOM:

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

docker compose up 失败,并显示 Error response from daemon: driver failed programming external connectivity ... bind: address already in use。 已有其他进程占用 port 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 需要多少 RAM?

实际最低配置建议为 4 GB,团队负载较高时建议使用 8 GB。Rocket.Chat 的 Node 进程约占用 1 到 1.5 GB,MongoDB 的默认 WiredTiger 缓存大小为总 RAM 减去 1 GB 后的一半,计算时相当于服务器上没有运行其他程序。在 2 GB 的服务器上,该缓存、Node 堆以及 mongod 自身的开销会相互争用内存;在实际负载下,内存不足终止程序会终止 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 主版本。Rocket.Chat 会在启动时运行迁移,其文档警告跳过主版本可能导致严重的兼容性问题,但不会保证阻止此类升级;在更新固定的镜像标签前,应先阅读对应版本的发布说明。使用 curl -s https://releases.rocket.chat/<version>/info | jq .compatibleMongoVersions 检查目标版本支持的 MongoDB 版本。升级 MongoDB 时,每次只跨越一个主版本,并在每次升级后使用 confirm: true 设置 setFeatureCompatibilityVersion。始终先创建 mongodump。