Chatwoot VPS Docker Compose 自架教學
使用 Docker Compose 與 Traefik 在 VPS 部署 Chatwoot,固定映像標籤、設定可正常寄信的 SMTP,並備份 Postgres 與上傳檔案,安全完成升級。
建置內容
若要在 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 個核心則適用於每天最多 20,000 個對話。此外,頁面要求至少 1 GB swap,並直接說明原因:避免機器在升級期間耗盡記憶體。Postgres 至少預留 5 GB 到 10 GB 磁碟空間,檔案上傳所需空間另計。
先說結論。2 GB VPS 可以啟動 Chatwoot,在只有 2 個代理人且收件匣流量很低時,看起來也能正常運作。但它會在兩種情況下失效。第一種是 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 表示程序遭 signal 9 終止。使用 sudo dmesg -T | grep -i "killed process" 即可確認,該指令會列出核心選中的程序。
如果 4 GB 超出預算,可以使用 2 GB 的機器並配置 2 GB swap,但必須接受高負載時回應時間會變長,而不是讓服務直接停止。無論採用哪種配置,為每項服務設定硬性記憶體上限都值得執行,避免 worker 連帶使資料庫停止。請參閱 Docker Compose 中的記憶體限制。
檔案上傳會持續增加,且不受你設定的上限限制。客戶附加的每張螢幕擷取畫面都會寫入儲存 volume 並保留在其中,因此請監控 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/storagelatest 表示下一次 docker compose pull 會取得當天發布的任何內容,這可能是包含您從未讀過的資料遷移作業之主要版本。Chatwoot 的資料遷移在實務上無法復原,因此意外升級後只能從備份還原,不能直接復原變更。請固定標籤,並在有意識的情況下修改它。截至 August 2026,v4.16.2 是目前版本;請查看 版本發布頁面,確認今天應固定的標籤。
base 服務是 YAML anchor,rails 和 sidekiq 都會合併使用它,因此只要在一處修改標籤,兩者都會套用變更。編輯檔案時,請一併刪除頂端的 version: '3' 行。現代 Compose 會忽略這一行,並在每個指令執行時顯示 the attribute 'version' is obsolete, it will be ignored。
填寫 .env 檔案
先產生 secret。上游要求使用英數字元值,因為值經過 shell 或 YAML parser 時,特殊字元可能會被錯誤處理。
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=localPOSTGRES_HOST=postgres 和 redis://redis:6379 是 Compose 服務名稱,會在專案的預設 network 上解析。FRONTEND_URL 不是裝飾用的設定。Chatwoot 會使用它建立 widget script URL,以及外寄 email 中的所有連結。因此,值設定錯誤時,密碼重設連結會指向無法回應的主機。
接下來是上游檔案中的陷阱。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 image 只會在初始化空的資料目錄時套用 POSTGRES_PASSWORD。之後變更該值不會生效,因為 initdb 不會再次執行。如果你已經啟動過此 stack,請直接在資料庫內變更密碼。
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 的人都能在你的支援服務台註冊。之後 agent 會透過邀請加入,密碼也只會儲存在這個應用程式中。當你執行約半打服務,並開始厭倦在每個服務中維護獨立的帳戶清單時,像 Authentik 這類自架 identity provider 就能取代這些獨立帳戶。
.env 現在以明文儲存此 stack 的所有 secret,因此請將檔案權限設為 mode 600,並避免提交至 git。Compose 如何讀取 env 檔案,以及 secret 會在哪裡洩漏 說明其中的風險,包括 env_file 與 environment 之間的差異。
將 Chatwoot 放在現有的 Traefik 後方
不要為單一應用程式建立第二個反向代理。如果 Traefik 已在這台伺服器上為其他容器處理 TLS termination,Chatwoot 只需透過一組 label 加入即可。如果尚未設定,請先依照讓 Traefik 置於多個 Docker Compose 應用程式前方完成一次設定,再回到這裡。
保留上游的 docker-compose.yaml,盡量維持原樣,之後才能與較新的版本比對差異,並將變更放在 override 檔案中。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 network,這就是 proxy 項目的作用;同時也必須保留在 default 上,否則會失去 Postgres 和 Redis。第二行就是最容易被遺漏的設定。
請勿修改 ports: 區塊。上游將它繫結至 127.0.0.1:3000,這只接受 loopback 連線,因此無法從網際網路存取,也能繼續搭配 curl -I http://127.0.0.1:3000 在這台伺服器內部進行測試。
agent dashboard 會持續與 /cable 維持 websocket 連線,以即時接收訊息。Traefik 會直接轉送 HTTP upgrade,不需要額外設定,因此無須新增任何內容。如果之後在 Traefik 前方加入 CDN 或其他代理,請在該處允許 websockets,否則會出現 dashboard 能正常載入,但新訊息只有在手動重新整理後才會顯示的情況。
初始化資料庫並啟動服務堆疊
先啟動資料服務,等待 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這會在資料庫不存在時建立資料庫,然後載入結構描述與預設種子資料。執行時會輸出 migration 訊息,並正常結束。若畫面持續輸出 postgres:5432 - no response,表示 entrypoint 正在等待尚未接受連線的資料庫。首次執行時,通常代表 initdb 仍在執行。請等待、查看 Postgres 日誌,然後再次執行。如果在 vector extension 處停止,表示你已將 pgvector 映像檔替換成標準 Postgres 映像檔。
docker compose up -d
docker compose ps
docker compose logs --tail 30 rails4 個容器都應顯示 Up,而 rails 日誌最後應出現 Puma 正在監聽 http://0.0.0.0:3000 的訊息。接著檢查公開路徑:
curl -sI https://support.example.com | head -n 1HTTP/2 200 表示整條鏈路正常運作。Traefik 回傳 404 表示 router 規則未比對成功,通常是主機名稱拼寫錯誤。502 表示 Traefik 已比對到 router,但無法連線至容器,幾乎總是因為缺少 proxy network,或 loadbalancer.server.port 不是 3000。
開啟 URL,在 /app/auth/signup 建立帳戶,接著設定 ENABLE_ACCOUNT_SIGNUP=false,並執行 docker compose up -d 關閉表單。
沒有 SMTP 時,密碼重設與電子郵件對話為何會失效
未設定 SMTP(simple mail transfer protocol)的 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 埠的 relay 能夠建立連線。SMTP_DOMAIN 是伺服器在 SMTP 交握期間宣告的網域,部分 relay 會拒絕不相符的網域。
套用設定並監看 worker:
docker compose up -d rails sidekiq
docker compose logs -f sidekiq從登入頁面觸發密碼重設。成功傳送時,Sidekiq 日誌會顯示 mailer 工作正常完成。傳送失敗時,日誌會先顯示例外類別,接著顯示 Sidekiq 以逐漸增加的退避時間重試。因此,設定錯誤的 relay 會在數小時內每隔幾分鐘產生相同錯誤。
常見的拒絕有兩種,兩者都不是 Chatwoot 的錯誤。535 Authentication failed 表示該 relay 的使用者名稱或密碼錯誤,而許多供應商要求使用應用程式密碼,而非帳戶密碼。550 Sender address rejected 表示 MAILER_SENDER_EMAIL 是 relay 不允許用來寄信的地址,因此必須使用已向該供應商驗證過的信箱或網域。
將電子郵件接收至對話是另一項工作。這需要 MAILER_INBOUND_EMAIL_DOMAIN 和 RAILS_INBOUND_EMAIL_SERVICE,以及一部會將傳入郵件交給 Chatwoot 的郵件伺服器。租用 relay 是最快的方式。如果希望自行管理完整的郵件流程,使用 Mailcow 執行自有郵件伺服器 會說明這項承諾實際涉及的工作。
要備份哪些內容,以及如何證明還原可正常運作
Chatwoot 備份包含 4 個部分。少了其中任何一項,還原就會變成重新建置。
- Postgres 資料庫,其中包含對話、聯絡人、代理人帳號及所有設定。
storage_datavolume,因為ACTIVE_STORAGE_SERVICE=local會將上傳檔案寫入磁碟,只在 Postgres 中保存參照資料列。.env檔案,因為其中保存SECRET_KEY_BASE與ACTIVE_RECORD_ENCRYPTION_*金鑰。- compose 檔案,因為其中記錄資料庫 schema 所對應的確切 image tag。
只還原資料庫時,所有對話雖然會回來,但附件都會失效,因為資料列指向的檔案已不在磁碟上。
cd ~/chatwoot
docker compose exec -T postgres pg_dump -U postgres -Fc chatwoot > db-$(date +%F).dump-T 很重要。未指定它時,Compose 會配置 pseudo terminal,進而改寫串流中的換行位元組,因此產生的 dump 檔案會被 pg_restore 拒絕。-Fc 是 custom format,具備壓縮功能,也能讓 pg_restore 選擇性處理資料。
docker run --rm -v chatwoot_storage_data:/data:ro -v "$PWD":/backup alpine \
tar czf /backup/storage-$(date +%F).tgz -C /data .volume 名稱是專案目錄名稱加上 _storage_data。在信任該命令前,請先使用 docker volume ls | grep storage_data 確認名稱,因為指定不存在的 volume 時,Docker 會建立空的 volume,而不會回報錯誤。最後會得到有效但空白的 archive,完全不會出現錯誤。完成後使用 ls -lh storage-*.tgz 檢查大小。
現在這兩個檔案仍與受其保護的資料位於同一個磁碟上,因此沒有提供任何保護。請將它們傳送到主機外並加密,因為 database dump 以純文字包含所有客戶訊息。使用 restic 加密並備份至異地說明排程與保留策略。
還原演練,請在真正需要前先執行
請還原到第二台 VPS,不要還原到正式環境。將 .env、compose 檔案及兩個 archive 複製過去,然後執行:
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 進行還原時,所有 session cookie 都會失效,因此所有人都會被登出。使用不同的 ACTIVE_RECORD_ENCRYPTION_* 金鑰進行還原則更嚴重:Chatwoot 無法解密保存頻道認證資訊的欄位,並會產生 ActiveRecord::Encryption::Errors::Decryption。因此 .env 必須列入備份清單。
如何將 Chatwoot 升級至新的 tag
執行順序比指令本身更重要。
- 閱讀目前 tag 與目標 tag 之間的 release notes,確認是否需要手動執行其他步驟。
- 建立最新的資料庫 dump 與儲存區 archive,並確認兩者的檔案大小合理。
- 修改
docker-compose.yaml中baseservice 的 image tag。 - Pull 新的 image、停止 stack、執行 migrations,然後再次啟動。
docker compose pull
docker compose down
docker compose run --rm rails bundle exec rails db:chatwoot_prepare
docker compose up -d
docker compose images請在執行 migration 前先 pull,因為 migration 必須由新的 image 執行。舊 image 不包含新的 migration files。執行 migration 前先停止 stack,因為舊程式碼與新 schema 不相容;執行中的舊 Rails process 可能會產生錯誤,或寫入新 schema 不接受的資料列。停止 stack 也會釋放 migration 所需的記憶體,這正是 upstream 要求設定 swap 的原因。
docker compose images 會顯示每個 container 實際執行的 tag,可用來找出已修改 tag 但忘記 pull 的情況。
不要一次跨越多個版本。針對舊版安裝,upstream 建議逐一通過中間的 tag,因為 migration 整合至 base schema 後就會被移除,過舊的資料庫可能因此進入無法繼續升級的狀態。每次只升級一個 minor version,並在每次升級後執行 prepare step。
如果 Rails 在 migration 執行前啟動,會拒絕提供服務,並記錄 ActiveRecord::PendingMigrationError: Migrations are pending。設定 restart: always 後,container 會持續重新啟動,因此 docker compose ps 顯示的 uptime 會每隔幾秒重設一次。執行 prepare step 後即可清除這個問題。
回復版本表示重新設定舊的 tag,並還原 dump。沒有可依賴的 reverse migration path,這就是第 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 資料 volume 中預先寫入的密碼不同。請在執行中的容器內使用 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?
截至 2026 年 8 月,上游要求最低 4 GB RAM 與 4 個 CPU 核心,標示每日最多可處理 10,000 個對話;若要處理最多 20,000 個對話,則需要 8 GB RAM 與 8 個核心。至少增加 1 GB swap,因為升級時會啟動第二個 Rails 程序來套用 migration,小型主機通常會在這個階段耗盡記憶體。2 GB VPS 可以啟動並供幾位客服人員使用,但 Sidekiq 在負載下單獨就可能使用超過 1 GB,因此在繁忙時段及升級期間,預期容器可能會以 exit code 137 被終止。
為什麼 Chatwoot 的密碼重設郵件一直收不到?
因為未設定 SMTP,ActionMailer 會嘗試將郵件傳送到 localhost 的 25 埠,但容器內沒有郵件伺服器。工作會在 Sidekiq 中以 Errno::ECONNREFUSED: Connection refused - connect(2) for "localhost" port 25 失敗,而瀏覽器仍會顯示成功訊息。在 .env 中設定 SMTP_ADDRESS、SMTP_PORT、SMTP_USERNAME、SMTP_PASSWORD 與 MAILER_SENDER_EMAIL,重新啟動 rails 與 sidekiq 服務,然後觸發密碼重設並監控 docker compose logs -f sidekiq。
要完整還原 Chatwoot,需要備份哪些項目?
Postgres 資料庫、storage_data Docker volume、.env 檔案,以及 compose 檔案。只有資料庫並不足夠,因為上傳的檔案儲存在 volume 中,而 Postgres 只保存這些檔案的參照;因此只還原資料庫會得到附件已損壞的對話。.env 很重要,因為不同的 SECRET_KEY_BASE 會使所有使用者登出,而不同的 ACTIVE_RECORD_ENCRYPTION_* 金鑰會導致加密欄位無法讀取。
如何升級 Chatwoot 而不破壞資料庫?
先建立備份,修改 compose 檔案中的 image tag,然後執行 docker compose pull、docker compose down、docker compose run --rm rails bundle exec rails db:chatwoot_prepare 與 docker compose up -d。先 pull,因為 migration 必須由新版 image 執行;也要先停止 stack,因為舊程式碼搭配新版 schema 會產生錯誤。對於較舊的安裝,請一次升級一個 minor version,因為 migration 一旦整合至基礎 schema,就可能被移除。
可以使用標準 postgres image 取代 pgvector 嗎?
不可以。Chatwoot 的 schema 會啟用 vector extension,因此標準的 postgres image 會在執行 db:chatwoot_prepare 時因 ERROR: extension "vector" is not available 而失敗,原因是該 image 中沒有這個 extension 的 control file。保留上游 compose 檔案中的 pgvector/pgvector:pg16,或使用針對 Postgres major version 提供 pgvector 的其他 image。