SSD Nodes Learn 🎉 VPS $5.50/月起
指南 Matt Connor作者: Matt Connor · 更新于 2026-08-11

用 Docker 在 VPS 上自托管 Chatwoot

通过 Docker Compose 和 Traefik 部署 Chatwoot,固定镜像标签,配置可正常发信的 SMTP,并备份 Postgres 与上传文件。最低建议 4 GB 内存、4 个 CPU 核心和 1 GB swap,安全完成升级。

构建内容

要在 VPS 上自行托管 Chatwoot,需要运行 4 个容器:Rails Web 进程、Sidekiq 后台工作进程、带 pgvector 扩展的 PostgreSQL,以及 Redis。Chatwoot 是一个开源客户支持服务台,因此您可以在自己控制的服务器上获得共享团队收件箱和网站聊天小组件。安装大约需要 20 分钟。之后的邮件投递、备份、升级和资源配置,才决定它能否在 1 年后仍正常运行。

每个容器只负责一项工作。Rails 提供客服代理控制台和小组件 API(application programming interface)。Sidekiq 处理耗时任务:发送邮件、轮询已连接的渠道、运行自动化规则和生成报告。Postgres 保存会话、联系人、客服代理账户,以及您在控制台中修改的所有设置。Redis 保存 Sidekiq 队列和 ActionCable pub/sub 通道,用于在打开的控制台中推送新消息,而无需重新加载页面。Redis 在这里不是可随时丢弃的缓存,因为丢失 Redis 就意味着丢失排队中的任务。

上游 compose 文件中的 Postgres 镜像是 pgvector/pgvector:pg16,而不是标准的 postgres 镜像,因为 Chatwoot 的数据库架构为其 AI 功能启用了 vector 扩展。换用标准 Postgres 后,首次运行数据库会因 ERROR: extension "vector" is not available 而停止,因为该镜像中没有扩展的控制文件。请使用上游发布的镜像。

本指南假设服务器上的 Docker 和反向代理已经可以正常工作。如果尚未配置,请先阅读 VPS 上的 Docker Compose,然后返回本指南。

自托管 Chatwoot 需要多大的 VPS?

截至 2026 年 8 月,上游需求页面要求最低配置为 4 GB RAM 和 4 个 CPU 核心,并称该配置每天最多可处理 10,000 个会话。该页面将 8 GB RAM 和 8 个 CPU 核心对应到每天最多 20,000 个会话。它还要求至少 1 GB swap,并直接说明原因:确保升级期间机器不会耗尽内存。计算磁盘空间时,在文件上传之外,还应为 Postgres 预留 5 GB 到 10 GB。

下面直接说明实际情况。2 GB VPS 可以启动 Chatwoot,在只有两个坐席且收件箱负载较低时看起来也没有问题。但它通常会在两个场景中崩溃。第一个是 Sidekiq:上游测得繁忙服务器上的 Sidekiq 内存占用超过 1 GB,因此一批邮件或报表任务就可能让机器在 Rails、Postgres 和 Redis 占用各自所需内存前耗尽可用内存。第二个是升级,因为 db:chatwoot_prepare 会启动新的 Rails 进程来执行迁移,而该镜像中的 Rails 启动过程在执行实际任务前就会占用数百 MB。

系统不会先发出明确警告。内核的 out of memory killer 会向占用内存最多的进程发送 SIGKILL,Docker 发现容器退出后,restart: always 会再次启动它。docker compose ps 随后会显示容器持续返回 Exited (137);其中 137 表示进程被信号 9 终止。使用 sudo dmesg -T | grep -i "killed process" 可以确认这一点,该命令会显示内核选择终止的进程。

如果 4 GB 超出预算,可以使用带 2 GB swap 的 2 GB VPS,但要接受服务在负载升高时响应变慢,而不是直接退出。无论采用哪种配置,都建议为每个服务设置硬内存上限,避免 worker 连带使数据库停止运行。请参阅 Docker Compose 中的内存限制

文件上传会在没有预设上限的情况下持续增长。客户上传的每张截图都会写入存储卷并一直保留,因此应监控 docker system df -v,不要想当然地认为是数据库占满了磁盘。

获取 Compose 文件并固定版本标签

mkdir -p ~/chatwoot && cd ~/chatwoot
wget -O .env https://raw.githubusercontent.com/chatwoot/chatwoot/develop/.env.example
wget -O docker-compose.yaml https://raw.githubusercontent.com/chatwoot/chatwoot/develop/docker-compose.production.yaml
chmod 600 .env

刚刚下载的文件中写有 image: chatwoot/chatwoot:latest。在执行其他操作前,先修改这一项。

services:
  base: &base
    image: chatwoot/chatwoot:v4.16.2
    env_file: .env
    volumes:
      - storage_data:/app/storage

latest 表示下一个 docker compose pull 会获取当天发布的任意版本,其中可能包含你完全不了解的迁移所对应的主版本。实际上,Chatwoot 迁移无法回滚,因此意外升级后只能从备份恢复,不能直接撤销。请固定标签,并有意进行修改。截至 2026 年 8 月,v4.16.2 是当前版本;请查看版本发布页面,确认今天应固定的标签。

base 服务是一个 YAML 锚点,railssidekiq 都会合并该锚点,因此在一个位置修改标签后,两个服务都会使用新标签。编辑文件时,同时删除顶部的 version: '3' 行。现代 Compose 会忽略该行,并在每次执行命令时输出 the attribute 'version' is obsolete, it will be ignored

填写 .env 文件

先生成密钥。上游要求使用字母数字值,因为该值经过 shell 或 YAML 解析器时,特殊字符可能会被错误处理。

head /dev/urandom | tr -dc A-Za-z0-9 | head -c 63 ; echo ''

然后在 .env 中设置以下键。

SECRET_KEY_BASE=<the 63 characters you just generated>
FRONTEND_URL=https://support.example.com
FORCE_SSL=true
DEFAULT_LOCALE=en
ENABLE_ACCOUNT_SIGNUP=true

POSTGRES_HOST=postgres
POSTGRES_USERNAME=postgres
POSTGRES_PASSWORD=<long random string>
POSTGRES_DATABASE=chatwoot

REDIS_URL=redis://redis:6379
REDIS_PASSWORD=<a different long random string>

RAILS_ENV=production
INSTALLATION_ENV=docker
ACTIVE_STORAGE_SERVICE=local

POSTGRES_HOST=postgresredis://redis:6379 是 Compose 服务名称,可在项目的默认网络中解析。FRONTEND_URL 不是装饰性配置。Chatwoot 会根据它生成小组件脚本 URL,以及外发邮件中的所有链接。因此,值不正确会导致密码重置链接指向无法响应的主机。

现在说明上游文件中的一个易错点。postgres 服务不会读取 .env。它有自己的 environment 配置块,其中 POSTGRES_PASSWORD= 留空。因此,仅在 .env 中设置密码会导致数据库没有密码,而应用配置了密码。将该服务指向同一个变量:

  postgres:
    image: pgvector/pgvector:pg16
    restart: always
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_DB=chatwoot
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}

Compose 会从项目目录读取 .env,用于 ${...} 替换,因此两处现在会获得相同的字符串。如果这里配置错误,Rails 会因 PG::ConnectionBad: FATAL: password authentication failed for user "postgres" 停止运行。

有一个行为几乎总会让人意外:Postgres 镜像只会在初始化空数据目录时应用 POSTGRES_PASSWORD。之后修改该值不会生效,因为 initdb 不会再次运行。如果您已经启动过一次此堆栈,请直接在数据库中修改密码。

docker compose exec postgres psql -U postgres -c "ALTER USER postgres WITH PASSWORD 'the-new-password';"

ENABLE_ACCOUNT_SIGNUP=true 是临时配置。它会开放公开注册表单,以便您创建第一个账户。账户创建后,将其设置为 false,并立即再次运行 docker compose up -d;否则,任何找到该 URL 的人都可以在您的支持平台上注册。之后,代理用户通过邀请加入,密码只保存在这个应用中。当您运行近 6 个服务,不想在每个服务中维护独立的账户列表时,这种方式就不够方便了。此时,类似 Authentik 的自托管身份提供商可以替代这些独立账户列表。

.env 现在以明文保存此堆栈中的所有密钥,因此请将文件权限设为 600,并确保不要将其提交到 git。Compose 如何读取 env 文件,以及密钥会在哪里泄露介绍了其中的风险,包括 env_fileenvironment 之间的区别。

将 Chatwoot 接入现有 Traefik

不要为一个应用再部署第二个反向代理。如果 Traefik 已在此服务器上为其他容器终止 TLS(传输层安全),Chatwoot 只需通过标签块接入。如果尚未配置 Traefik,请先按照在多个 Docker Compose 应用前部署 Traefik完成一次配置,然后返回此处。

保留上游的 docker-compose.yaml,尽量不修改原始内容,以便稍后与新版副本进行差异比较,并将修改放入覆盖文件。Compose 会自动合并 docker-compose.override.yaml将 Compose 配置拆分到多个文件介绍了合并规则。

services:
  rails:
    networks:
      - default
      - proxy
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.chatwoot.rule=Host(`support.example.com`)"
      - "traefik.http.routers.chatwoot.entrypoints=websecure"
      - "traefik.http.routers.chatwoot.tls.certresolver=letsencrypt"
      - "traefik.http.services.chatwoot.loadbalancer.server.port=3000"

networks:
  proxy:
    external: true

请使用您自己的 entrypoint 和 certresolver 名称。容器必须与 Traefik 位于同一个 Docker 网络中,proxy 条目负责实现这一点;同时也必须保留在 default 上,否则会失去对 Postgres 和 Redis 的访问。第二行最容易被遗漏。

不要修改 ports: 块。上游将其绑定到 127.0.0.1:3000,该地址仅供回环访问,因此不会从互联网访问到它;同时,您仍可通过 curl -I http://127.0.0.1:3000 在服务器内部进行测试。

代理仪表板会与 /cable 保持 websocket 连接,以实时接收消息。Traefik 无需额外配置即可转发 HTTP upgrade,因此不需要添加任何内容。如果以后在 Traefik 前面部署 CDN 或其他代理,请在该层允许 websockets;否则仪表板可以正常加载,但新消息只有在手动刷新后才会显示。

初始化数据库并启动服务栈

先启动数据服务,并等待 Postgres 完成首次初始化。

docker compose up -d postgres redis
docker compose logs postgres | tail -n 5

等待 database system is ready to accept connections。然后创建数据库架构。

docker compose run --rm rails bundle exec rails db:chatwoot_prepare

此命令会在数据库不存在时创建数据库,然后加载数据库架构和默认种子数据。命令会输出迁移记录,并正常退出。如果命令持续输出 postgres:5432 - no response,说明入口点正在等待尚未接受连接的数据库。首次运行时,通常表示 initdb 仍在运行。请等待并查看 Postgres 日志,然后再次运行命令。如果命令在 vector 扩展处停止,说明您已将 pgvector 镜像替换为标准 Postgres 镜像。

docker compose up -d
docker compose ps
docker compose logs --tail 30 rails

4 个容器都应显示 Up,并且 rails 日志末尾应出现 Puma 正在监听 http://0.0.0.0:3000 的记录。然后检查公网访问路径:

curl -sI https://support.example.com | head -n 1

HTTP/2 200 表示整条链路正常。Traefik 返回 404 表示路由规则未匹配,通常是主机名拼写错误。返回 502 表示 Traefik 已匹配路由,但无法连接容器。这几乎总是因为缺少 proxy 网络,或 loadbalancer.server.port 不是 3000。

打开 URL,在 /app/auth/signup 创建账户,然后设置 ENABLE_ACCOUNT_SIGNUP=false 并运行 docker compose up -d 关闭表单。

没有 SMTP 时,密码重置和邮件会话为何会失败

未配置 SMTP(简单邮件传输协议)的 Chatwoot 是一个无法发送邮件的客服系统,这会导致不只是通知功能失效。密码重置会停止工作,因此被锁定的管理员无法重新获得访问权限。坐席邀请也会停止工作,因为邀请依赖电子邮件。回复邮件会话中的客户也会失败,因此会话只能单向进行。这是很多人会跳过的步骤,直到最需要时才发现问题。

其机制很简单。未配置 SMTP 时,ActionMailer 会保留默认设置,将邮件投递到 localhost 的 25 端口。Rails 容器内没有邮件服务器,因此投递任务会抛出 Errno::ECONNREFUSED: Connection refused - connect(2) for "localhost" port 25。邮件由后台任务发送,因此这条错误会写入 Sidekiq 日志,而不会写入 Rails 日志。同时,点击“忘记密码”的用户会看到成功提示,却收不到邮件。

MAILER_SENDER_EMAIL=Support <support@example.com>
SMTP_DOMAIN=example.com
SMTP_ADDRESS=smtp.example.com
SMTP_PORT=587
SMTP_USERNAME=support@example.com
SMTP_PASSWORD=<the relay password>
SMTP_AUTHENTICATION=plain
SMTP_ENABLE_STARTTLS_AUTO=true

使用 587 端口和 STARTTLS。它会先以明文建立连接,再在身份验证前升级为加密连接。大多数 VPS 提供商会阻止出站 25 端口,以限制垃圾邮件,因此 587 端口上的中继通常是唯一能够建立连接的方式。SMTP_DOMAIN 是服务器在 SMTP 会话期间声明的域名,部分中继会拒绝不匹配的域名。

应用设置并监控 worker:

docker compose up -d rails sidekiq
docker compose logs -f sidekiq

从登录页面触发密码重置。投递正常时,Sidekiq 日志会显示邮件任务正常完成。投递失败时,日志会显示异常类,随后 Sidekiq 会以逐渐增加的退避时间重试。因此,中继配置错误会导致相同错误每隔几分钟出现一次,并持续数小时。

常见的拒绝有两种,但都不是 Chatwoot 的错误。535 Authentication failed 表示该中继不接受提供的用户名或密码,许多提供商要求使用应用专用密码,而不是账户密码。550 Sender address rejected 表示 MAILER_SENDER_EMAIL 是中继不允许用来发件的地址,因此它必须是已通过该提供商验证的邮箱地址或域名。

接收邮件并写入会话是另一项独立工作。它需要 MAILER_INBOUND_EMAIL_DOMAINRAILS_INBOUND_EMAIL_SERVICE,还需要邮件服务器将收到的邮件交给 Chatwoot。租用邮件中继是最快的方案。如果您希望自行管理完整的邮件链路,请参阅使用 Mailcow 运行自己的邮件服务器,了解这项工作实际需要承担的内容。

备份哪些内容,以及如何验证恢复有效

Chatwoot 备份包含 4 个部分,遗漏任何一项都会让恢复变成重建。

  • Postgres 数据库,其中保存对话、联系人、客服账号和所有设置。
  • storage_data 卷,因为 ACTIVE_STORAGE_SERVICE=local 会将上传的文件写入磁盘,只在 Postgres 中保存引用记录。
  • .env 文件,因为其中保存 SECRET_KEY_BASEACTIVE_RECORD_ENCRYPTION_* 密钥。
  • compose 文件,因为其中记录数据库架构所匹配的确切镜像标签。

只恢复数据库时,所有对话都会恢复,但附件会损坏,因为这些记录指向的文件已经不在磁盘上。

cd ~/chatwoot
docker compose exec -T postgres pg_dump -U postgres -Fc chatwoot > db-$(date +%F).dump

-T 很重要。不使用它时,Compose 会分配伪终端,导致流中的换行字节被改写,因此生成的转储文件会被 pg_restore 拒绝。-Fc 是自定义格式,支持压缩,并允许 pg_restore 有选择地执行操作。

docker run --rm -v chatwoot_storage_data:/data:ro -v "$PWD":/backup alpine \
  tar czf /backup/storage-$(date +%F).tgz -C /data .

卷名称由项目目录名加上 _storage_data 组成。在信任该命令前,使用 docker volume ls | grep storage_data 确认卷名称,因为指定不存在的卷时,Docker 会创建空卷,而不是报错。这样会得到一个有效的空归档,且完全不会出现错误。之后使用 ls -lh storage-*.tgz 检查大小。

现在这两个文件仍与受其保护的数据位于同一磁盘上,这并不能提供任何保护。应将它们传到服务器之外并加密,因为数据库转储包含所有客户消息的明文。使用 restic 创建加密的异地备份介绍计划任务和保留策略。

在需要恢复之前先执行恢复演练

将备份恢复到第二台 VPS,而不是线上服务器。复制 .env、compose 文件和两个归档,然后运行:

docker compose up -d postgres
docker compose exec -T postgres pg_restore -U postgres -d chatwoot --clean --if-exists < db-2026-08-10.dump
docker run --rm -v chatwoot_storage_data:/data -v "$PWD":/backup alpine \
  sh -c 'rm -rf /data/* && tar xzf /backup/storage-2026-08-10.tgz -C /data'
docker compose up -d

--clean --if-exists 会在加载前删除现有对象,因此只能将其用于可以接受数据丢失的数据库。然后登录并打开一个带附件的对话。如果消息列表可以加载,且文件可以下载,则说明备份有效。

使用不同的 SECRET_KEY_BASE 恢复会使所有会话 Cookie 失效,所有用户都会被登出。使用不同的 ACTIVE_RECORD_ENCRYPTION_* 密钥则更严重:Chatwoot 无法解密保存频道凭据的列,并会抛出 ActiveRecord::Encryption::Errors::Decryption。因此,.env 必须列入备份清单。

如何将 Chatwoot 升级到新标签

顺序比命令本身更重要。

  1. 阅读当前标签与目标标签之间的发布说明,确认是否需要执行手动步骤。
  2. 创建最新的数据库转储和存储归档,并检查两个文件的大小是否合理。
  3. docker-compose.yaml 中修改 base 服务的镜像标签。
  4. 拉取新镜像,停止栈,运行迁移,然后再次启动。
docker compose pull
docker compose down
docker compose run --rm rails bundle exec rails db:chatwoot_prepare
docker compose up -d
docker compose images

在运行迁移前先拉取镜像,因为迁移必须使用新镜像运行:旧镜像不包含新的迁移文件。在运行迁移前停止栈,因为旧代码与新架构不一致,运行中的旧 Rails 进程可能报错,或写入新架构不接受的数据行。停止栈还会释放迁移所需的内存,这正是上游要求配置 swap 的原因。

docker compose images 会输出每个容器实际运行的标签,因此可以发现已修改标签但忘记拉取镜像的情况。

不要一次跨越多个版本升级。对于旧安装,上游建议逐步经过中间标签,因为迁移在合并到基础架构后会被移除,过旧的数据库可能因此进入无法继续迁移的状态。每次只升级一个次要版本,并在每次升级后运行 prepare 步骤。

如果 Rails 在迁移运行前启动,它会拒绝提供服务,并记录 ActiveRecord::PendingMigrationError: Migrations are pending。设置 restart: always 后,容器会不断重启,因此 docker compose ps 显示的运行时间会每隔几秒重置。运行 prepare 步骤后,该问题会被清除。

回滚意味着恢复旧标签,并还原数据库转储。没有可以依赖的反向迁移路径,这正是第 2 步的作用。

故障模式及您将看到的字符串

Traefik 返回 502 Bad Gateway。 路由器已匹配,但后端没有响应。检查 docker compose ps 是否将 rails 显示为 Up,然后运行 docker network inspect proxy,确认 rails 容器出现在其容器列表中。未连接到网络的容器对 Traefik 不可见,因此请求会匹配路由器,但随后无法到达任何位置。

仪表板可以加载,但新消息需要刷新页面。/cable 的 websocket 未能通过,或者 FRONTEND_URL 与浏览器地址栏中的地址不匹配。不匹配意味着页面尝试连接到其他源的 websocket,而浏览器会阻止该连接。

FATAL: password authentication failed for user "postgres" .env 中的密码与 Postgres 数据卷中预置的密码不同。请在运行中的容器内使用 ALTER USER 修复,因为再次编辑 .env 不会更改已经初始化的数据库。

NOAUTH Authentication required. Redis 使用 --requirepass 运行,但应用在未提供密码的情况下建立了连接,因此 REDIS_PASSWORD.env 中缺失,或未被加载。直接使用 docker compose exec redis redis-cli -a "$REDIS_PASSWORD" ping 测试,该命令应返回 PONG

容器以代码 137 退出。 这表示 SIGKILL;在小型服务器上,通常是内核的 out of memory killer 终止了进程。添加 swap,为各项服务设置内存限制,或迁移到更大规格的方案。

FAQ

自托管 Chatwoot VPS 需要多少 RAM?

截至 August 2026,上游要求最低 4 GB RAM 和 4 个 CPU 核心,支持每天最多 10,000 个会话;如果要支持最多 20,000 个会话,则需要 8 GB RAM 和 8 个核心。至少增加 1 GB swap,因为升级时会启动第二个 Rails 进程来执行迁移,小规格 VPS 通常会在此时耗尽内存。2 GB VPS 可以启动并供少量客服使用,但 Sidekiq 在负载下单独就可能超过 1 GB,因此在繁忙时段和升级期间,容器可能会被终止,并返回退出码 137。

为什么 Chatwoot 的密码重置邮件始终收不到?

因为未配置 SMTP 设置,ActionMailer 会尝试通过端口 25 投递到 localhost,而容器内部没有邮件服务器。该任务会在 Sidekiq 中以 Errno::ECONNREFUSED: Connection refused - connect(2) for "localhost" port 25 失败,但浏览器仍会显示成功消息。在 .env 中设置 SMTP_ADDRESSSMTP_PORTSMTP_USERNAMESMTP_PASSWORDMAILER_SENDER_EMAIL,重启 rails 和 sidekiq 服务,然后触发密码重置并监控 docker compose logs -f sidekiq

要备份哪些内容才能恢复 Chatwoot?

需要备份 Postgres 数据库、storage_data Docker 卷、.env 文件和 compose 文件。仅备份数据库是不够的,因为上传的文件存储在该卷中,而 Postgres 只保存这些文件的引用;因此,仅恢复数据库会得到附件已损坏的会话。.env 很重要,因为不同的 SECRET_KEY_BASE 会使所有用户退出登录,而不同的 ACTIVE_RECORD_ENCRYPTION_* 密钥会导致加密列无法读取。

如何升级 Chatwoot 而不破坏数据库?

先执行备份,再修改 compose 文件中的镜像标签,然后运行 docker compose pulldocker compose downdocker compose run --rm rails bundle exec rails db:chatwoot_preparedocker compose up -d。先拉取镜像,因为迁移必须使用新镜像执行;并先停止堆栈,因为旧代码访问新 schema 会产生错误。对于较旧的安装,应每次只跨一个 minor 版本升级,因为迁移合并到基础 schema 后会被移除。

可以使用标准 postgres 镜像代替 pgvector 吗?

不可以。Chatwoot 的 schema 启用了 vector 扩展,因此标准 postgres 镜像会在执行 db:chatwoot_prepare 时因 ERROR: extension "vector" is not available 失败,因为该镜像中不存在此扩展的控制文件。保留上游 compose 文件中的 pgvector/pgvector:pg16,或者使用另一个为您的 Postgres major 版本提供 pgvector 的镜像。

#chatwoot#自托管#docker-compose#support-desk#smtp#备份