Tự host Chatwoot trên VPS bằng Docker
Triển khai Chatwoot trên VPS bằng Docker Compose và Traefik, ghim tag image, cấu hình SMTP gửi được, backup Postgres và uploads, nâng cấp an toàn.
Bạn sẽ triển khai gì
Để tự host Chatwoot trên VPS, bạn chạy 4 container: tiến trình web Rails, worker nền Sidekiq, PostgreSQL có extension pgvector và Redis. Chatwoot là help desk hỗ trợ khách hàng mã nguồn mở. Vì vậy, bạn có inbox chung cho team và widget chat trên website, chạy trên server do bạn kiểm soát. Cài đặt mất khoảng 20 phút. Những việc sau đó, gồm gửi mail, backup, nâng cấp và tính toán tài nguyên, mới quyết định hệ thống có còn chạy sau 1 năm hay không.
Mỗi container chỉ đảm nhiệm một việc. Rails phục vụ dashboard của agent và API của widget. Sidekiq xử lý các tác vụ chậm: gửi email, polling các channel đã kết nối, chạy các rule tự động hóa và tạo report. Postgres lưu conversation, contact, tài khoản agent và mọi setting bạn thay đổi trong dashboard. Redis lưu các queue của Sidekiq và channel pub/sub của ActionCable. Channel này đẩy message mới vào dashboard đang mở mà không cần reload trang. Redis ở đây không phải cache có thể bỏ đi, vì mất Redis đồng nghĩa với mất các job đang chờ xử lý.
Image Postgres trong file compose upstream là pgvector/pgvector:pg16 thay vì image postgres mặc định, vì schema của Chatwoot bật extension vector cho các tính năng AI. Nếu thay bằng Postgres mặc định, lần chạy database đầu tiên sẽ dừng với lỗi ERROR: extension "vector" is not available, vì image đó không có control file của extension. Hãy dùng image được upstream phát hành.
Hướng dẫn này giả định Docker và reverse proxy trên máy chủ đã hoạt động. Nếu chưa, hãy bắt đầu với Docker Compose trên VPS rồi quay lại.
Chatwoot self-hosted cần VPS cấu hình thế nào?
Tính đến tháng 10 năm 2026, trang yêu cầu của upstream nêu mức tối thiểu là 4 GB RAM và 4 CPU core, phù hợp với tối đa 10,000 cuộc hội thoại mỗi ngày. Cấu hình 8 GB RAM và 8 core phù hợp với tối đa 20,000 cuộc hội thoại mỗi ngày. Trang này cũng yêu cầu ít nhất 1 GB swap và nêu rõ lý do: để máy không hết memory trong quá trình upgrade. Hãy dự trù 5 GB đến 10 GB disk cho Postgres, chưa tính file upload.
Nói thẳng: VPS 2 GB vẫn boot được Chatwoot và có vẻ ổn khi chỉ có 2 agent cùng inbox ít hoạt động. Nhưng nó sẽ gặp vấn đề ở 2 thời điểm. Thứ nhất là Sidekiq. Upstream đo được Sidekiq dùng hơn 1 GB trên server bận, nên một đợt email tăng đột biến hoặc một job tạo report có thể khiến máy hết memory trước khi Rails, Postgres và Redis dùng phần tài nguyên của chúng. Thứ hai là lúc upgrade, vì db:chatwoot_prepare boot một Rails process mới để chạy migration, trong khi việc boot Rails trên image này đã tốn hàng trăm MB trước khi thực hiện công việc hữu ích nào.
Bạn sẽ không nhận được cảnh báo rõ ràng trước khi lỗi xảy ra. Out of memory killer của kernel gửi SIGKILL đến process được chọn, thường là process lớn nhất. Docker thấy container dừng, rồi restart: always khởi động lại container đó. docker compose ps --all sau đó hiển thị một container liên tục restart hoặc nằm ở trạng thái Exited (137), trong đó 137 nghĩa là process bị kill bởi signal 9. Nếu bỏ --all, container đã dừng sẽ không được liệt kê. Chỉ riêng exit code 137 không chứng minh nguyên nhân là thiếu memory, vì docker kill cũng gửi cùng signal đó. Vì vậy, hãy xác nhận bằng sudo dmesg -T | grep -i "killed process" để biết process nào được kernel chọn.
Nếu 4 GB vượt ngân sách, bạn có thể chạy máy 2 GB với 2 GB swap và chấp nhận rằng response time sẽ tăng khi có tải. Swap chỉ là buffer giúp giảm nguy cơ hết memory, không phải bảo đảm. Một burst đủ lớn vẫn có thể khiến container bị kill. Dù dùng cấu hình nào, bạn nên đặt hard memory ceiling cho từng service để worker không kéo database xuống theo. Xem memory limit trong Docker Compose.
File upload là phần tăng dung lượng mà không có giới hạn nếu bạn không tự đặt. Mỗi screenshot customer đính kèm sẽ được ghi vào storage volume và nằm lại ở đó. Vì vậy, hãy theo dõi docker system df -v thay vì mặc định cho rằng database là nguyên nhân làm đầy disk.
Lấy file compose và cố định tag phiên bản
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 .envFile bạn vừa tải xuống ghi là image: chatwoot/chatwoot:latest. Hãy thay đổi dòng này trước khi làm bất kỳ việc gì khác.
services:
base: &base
image: chatwoot/chatwoot:v4.18.0
env_file: .env
volumes:
- storage_data:/app/storagelatest có nghĩa là lần chạy docker compose pull tiếp theo sẽ lấy bất kỳ phiên bản nào được publish vào sáng hôm đó. Phiên bản đó có thể là một major version kèm các migration mà bạn chưa đọc. Trên thực tế, migration của Chatwoot không thể hoàn tác, nên việc nâng phiên bản ngoài ý muốn sẽ buộc bạn phải restore từ backup thay vì undo. Hãy cố định tag và chỉ thay đổi khi bạn chủ động quyết định. v4.18.0 là release hiện tại tính đến October 2026, và release notes khuyến nghị dùng phiên bản này để nhận các bản sửa lỗi bảo mật; hãy kiểm tra trang releases để biết tag cần cố định hôm nay. Mọi ứng dụng quản lý database đều cần thận trọng như vậy. Vì thế, tự host trình theo dõi bài tập openGym bắt đầu bằng việc cố định một git tag, dù đây là ứng dụng nhỏ hơn nhiều so với ứng dụng hiện tại.
Service base là một YAML anchor được rails và sidekiq cùng merge. Vì vậy, thay đổi tag ở một vị trí sẽ áp dụng cho cả hai service. Trong lúc mở file, hãy xóa dòng version: '3' ở đầu file. Compose hiện đại bỏ qua dòng này và in the attribute 'version' is obsolete, it will be ignored trong mỗi command.
Điền file .env
Trước tiên, hãy tạo secret. Upstream yêu cầu giá trị chỉ gồm chữ và số, vì ký tự đặc biệt có thể bị biến dạng khi giá trị đi qua shell hoặc YAML parser.
head /dev/urandom | tr -dc A-Za-z0-9 | head -c 63 ; echo ''Sau đó đặt các key này trong .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 và redis://redis:6379 là tên Compose service, được phân giải trên network mặc định của project. FRONTEND_URL không phải thông tin trang trí. Chatwoot dùng nó để tạo URL của widget script và mọi liên kết trong email gửi đi. Vì vậy, giá trị sai sẽ tạo ra các liên kết đặt lại mật khẩu trỏ đến một host không phản hồi.
Đây là bẫy trong file upstream. Service postgres không đọc .env. Nó có block environment riêng, trong đó POSTGRES_PASSWORD= để trống. Vì vậy, chỉ đặt password trong .env sẽ khiến database không có password, còn application lại có password. Hãy trỏ service này đến cùng một biến:
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 đọc .env từ thư mục project để thực hiện việc thay thế ${...}. Vì vậy, cả hai phía sẽ nhận cùng một chuỗi. Nếu cấu hình sai, Rails sẽ dừng với PG::ConnectionBad: FATAL: password authentication failed for user "postgres".
Một hành vi khiến gần như mọi người bất ngờ: Postgres image chỉ áp dụng POSTGRES_PASSWORD khi khởi tạo một data directory trống. Thay đổi giá trị sau đó không có tác dụng, vì initdb không chạy lần thứ hai. Nếu bạn đã khởi động stack một lần, hãy thay đổi password trực tiếp trong database.
docker compose exec postgres psql -U postgres -c "ALTER USER postgres WITH PASSWORD 'the-new-password';"ENABLE_ACCOUNT_SIGNUP=true chỉ dùng tạm thời. Nó mở form đăng ký công khai để bạn tạo account đầu tiên. Hãy đặt nó thành false và chạy lại docker compose up -d ngay sau khi account của bạn được tạo. Nếu không, bất kỳ ai tìm thấy URL đều có thể đăng ký trên support desk của bạn. Sau đó, agent sẽ được thêm bằng invitation và password của họ chỉ được lưu trong app này. Cách này vẫn ổn cho đến khi bạn chạy khoảng nửa tá service và không muốn duy trì một danh sách account riêng cho từng service. Khi đó, một identity provider tự host như Authentik là thành phần thay thế các danh sách đó.
.env hiện chứa mọi secret của stack này ở dạng plain text. Hãy đặt permission của file ở mode 600 và không đưa file vào git. Cách Compose đọc env file và nơi secret có thể bị lộ trình bày các điểm cần chú ý, trong đó có sự khác nhau giữa env_file và environment.
Đặt Chatwoot phía sau Traefik hiện có
Không dựng thêm một reverse proxy cho một ứng dụng. Nếu Traefik đã thực hiện TLS termination (kết thúc lớp bảo mật truyền tải) cho các container khác trên máy này, Chatwoot chỉ cần tham gia bằng một block label. Nếu bạn chưa có cấu hình đó, hãy thiết lập một lần theo Traefik phía trước nhiều ứng dụng Docker Compose, rồi quay lại đây.
Giữ docker-compose.yaml của upstream gần với bản gốc để sau này có thể diff với bản mới hơn, và đặt các thay đổi của bạn trong một file override. Compose tự động merge docker-compose.override.yaml, còn tách Compose thành nhiều file giải thích các quy tắc merge.
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: trueDùng tên entrypoint và certresolver của riêng bạn. Container phải nằm trên cùng Docker network với Traefik; đó là tác dụng của entry proxy. Container cũng phải tiếp tục nằm trên default, nếu không nó sẽ mất kết nối đến Postgres và Redis. Đây là dòng mà nhiều người quên nhất.
Giữ nguyên block ports:. Upstream bind block này vào 127.0.0.1:3000, chỉ cho loopback truy cập, nên nó không thể truy cập từ Internet và vẫn hữu ích để kiểm thử từ bên trong máy bằng curl -I http://127.0.0.1:3000.
Dashboard của agent duy trì một websocket đến /cable để nhận message theo thời gian thực. Traefik chuyển tiếp HTTP upgrade mà không cần cấu hình thêm, nên không cần thêm gì. Nếu sau này bạn đặt một CDN hoặc proxy khác phía trước Traefik, hãy cho phép websocket ở đó. Dấu hiệu thường gặp là dashboard tải bình thường nhưng message mới chỉ xuất hiện sau khi refresh thủ công.
Khởi tạo database và khởi động stack
Khởi động các service dữ liệu trước và chờ Postgres hoàn tất lần chạy đầu tiên.
docker compose up -d postgres redis
docker compose logs postgres | tail -n 5Chờ database system is ready to accept connections. Sau đó tạo schema.
docker compose run --rm rails bundle exec rails db:chatwoot_prepareLệnh này tạo database nếu database chưa tồn tại, rồi nạp schema và dữ liệu seed mặc định. Lệnh in các dòng migration và thoát sạch. Nếu lệnh cứ in postgres:5432 - no response, entrypoint đang chờ một database chưa chấp nhận kết nối. Trong lần chạy đầu tiên, nguyên nhân thường là initdb vẫn đang xử lý. Hãy chờ, đọc log của Postgres, rồi chạy lại. Nếu lệnh dừng tại extension vector, bạn đã thay image pgvector bằng Postgres chuẩn.
docker compose up -d
docker compose ps
docker compose logs --tail 30 railsCả bốn container phải hiển thị Up, và log của rails phải kết thúc bằng một dòng Puma đang listening trên http://0.0.0.0:3000. Sau đó kiểm tra đường dẫn public:
curl -sI https://support.example.com | head -n 1HTTP/2 200 có nghĩa là toàn bộ chuỗi hoạt động đúng. Lỗi 404 từ Traefik nghĩa là router rule không khớp, thường do hostname bị gõ sai. Lỗi 502 nghĩa là Traefik đã khớp router nhưng không thể kết nối đến container. Nguyên nhân gần như luôn là thiếu network proxy hoặc loadbalancer.server.port không phải 3000.
Mở URL, tạo account tại /app/auth/signup, sau đó đặt ENABLE_ACCOUNT_SIGNUP=false và chạy docker compose up -d để đóng form.
Vì sao đặt lại mật khẩu và trao đổi email không hoạt động nếu thiếu SMTP
Chatwoot không có cấu hình SMTP (simple mail transfer protocol) là một hệ thống hỗ trợ không thể gửi email. Điều này làm hỏng nhiều chức năng hơn chỉ riêng notification. Chức năng đặt lại mật khẩu sẽ ngừng hoạt động, nên admin bị khóa tài khoản sẽ không thể đăng nhập lại. Lời mời agent cũng không hoạt động vì lời mời được gửi qua email. Việc trả lời khách hàng trong một cuộc trao đổi email cũng ngừng hoạt động, khiến cuộc trao đổi chỉ đi theo một chiều. Đây là bước nhiều người bỏ qua rồi chỉ phát hiện khi gặp sự cố nghiêm trọng nhất.
Cơ chế này khá đơn giản. Khi chưa đặt SMTP_ADDRESS, Chatwoot chuyển ActionMailer sang cơ chế gửi bằng sendmail. Tài liệu upstream gọi đây là thiết lập Postfix, là mặc định từ v2.12.0, và hướng dẫn cài Postfix trên server bằng sudo apt install -y postfix. Docker setup này không làm việc đó, nên không có mail server được cấu hình để cơ chế gửi mail chuyển email đến. Email được gửi từ background job, vì vậy mọi lỗi do cơ chế gửi tạo ra sẽ xuất hiện trong Sidekiq log, không xuất hiện trong Rails log. Trong khi đó, người bấm "quên mật khẩu" sẽ thấy thông báo xác nhận nhưng không nhận được email nào.
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=trueDùng port 587 với STARTTLS. Cơ chế này mở connection ở dạng plain text rồi nâng cấp lên encrypted trước khi authentication. Hầu hết nhà cung cấp VPS chặn outbound port 25 để hạn chế spam, nên relay trên port 587 thường là lựa chọn duy nhất có thể kết nối. SMTP_DOMAIN là domain server của bạn công bố trong quá trình SMTP conversation. Một số relay sẽ từ chối nếu giá trị này không khớp.
Áp dụng các setting rồi theo dõi worker:
docker compose up -d rails sidekiq
docker compose logs -f sidekiqThực hiện đặt lại mật khẩu từ login page. Nếu gửi mail hoạt động, mailer job sẽ hoàn tất bình thường trong Sidekiq log. Nếu bị lỗi, log sẽ hiển thị exception class, sau đó Sidekiq retry với backoff tăng dần. Vì vậy, một relay bị hỏng sẽ tạo cùng một lỗi vài phút một lần trong nhiều giờ.
Có 2 lỗi từ chối thường gặp, và không lỗi nào là bug của Chatwoot. 535 Authentication failed nghĩa là username hoặc password của relay không đúng. Nhiều nhà cung cấp yêu cầu application password thay vì password của tài khoản. 550 Sender address rejected nghĩa là MAILER_SENDER_EMAIL là địa chỉ mà relay không cho phép gửi dưới danh nghĩa địa chỉ đó. Vì vậy, địa chỉ này phải là mailbox hoặc domain đã được bạn xác minh với nhà cung cấp.
Nhận email vào một cuộc trao đổi là một tác vụ riêng. Tác vụ này cần MAILER_INBOUND_EMAIL_DOMAIN và RAILS_INBOUND_EMAIL_SERVICE, cùng với một mail server chuyển các message đến Chatwoot. Thuê một relay là cách nhanh nhất. Nếu bạn muốn tự quản lý toàn bộ đường đi của email, tự chạy mail server với Mailcow sẽ giải thích những gì cam kết này thực sự đòi hỏi.
Cần sao lưu những gì và cách xác minh bản khôi phục hoạt động
Một bản sao lưu Chatwoot gồm 4 phần. Bỏ qua bất kỳ phần nào sẽ biến việc khôi phục thành dựng lại hệ thống.
- Volume
storage_data, vìACTIVE_STORAGE_SERVICE=localghi các file đã tải lên vào disk và chỉ lưu một bản ghi tham chiếu trong Postgres. - Database Postgres, nơi lưu các cuộc hội thoại, contact, tài khoản agent và mọi setting.
- File
.env, vì file này chứaSECRET_KEY_BASEvà các keyACTIVE_RECORD_ENCRYPTION_*. - Các file compose, vì chúng ghi lại image tag chính xác tương ứng với schema database.
Nếu chỉ khôi phục database, mọi cuộc hội thoại sẽ trở lại nhưng attachment bị hỏng, vì các row trỏ đến những file không còn trên disk.
cd ~/chatwoot
docker compose exec -T postgres pg_dump -U postgres -Fc chatwoot > db-$(date +%F).dump-T rất quan trọng. Nếu thiếu tùy chọn này, Compose sẽ cấp phát pseudo terminal. Pseudo terminal thay đổi các byte newline trong stream, khiến bạn nhận được file dump bị pg_restore từ chối. -Fc là custom format. Format này nén dữ liệu và cho phép pg_restore khôi phục có chọn lọc.
docker run --rm -v chatwoot_storage_data:/data:ro -v "$PWD":/backup alpine \
tar czf /backup/storage-$(date +%F).tgz -C /data .Tên volume là tên thư mục project cộng với _storage_data. Hãy xác nhận tên này bằng docker volume ls | grep storage_data trước khi tin dùng command đó, vì Docker sẽ tạo một volume rỗng thay vì báo lỗi khi bạn chỉ định volume không tồn tại. Bạn sẽ nhận được một archive hợp lệ nhưng rỗng, hoàn toàn không có lỗi. Sau đó kiểm tra kích thước bằng ls -lh storage-*.tgz.
Cả hai file hiện nằm trên cùng disk với dữ liệu mà chúng bảo vệ, nên thực tế chúng không bảo vệ bạn khỏi sự cố nào. Hãy đẩy chúng ra khỏi máy chủ và mã hóa chúng, vì database dump chứa toàn bộ tin nhắn của khách hàng ở dạng plain text. Sao lưu off-site đã mã hóa bằng restic trình bày phần lập lịch và chính sách retention.
Quy trình khôi phục, hãy chạy trước khi cần
Khôi phục lên một VPS thứ hai, không khôi phục lên VPS đang chạy production. Copy .env, các file compose và cả hai archive sang đó, rồi chạy:
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 xóa các object hiện có trước khi nạp dữ liệu. Vì vậy, chỉ trỏ command này đến database mà bạn chấp nhận mất dữ liệu. Sau đó đăng nhập và mở một cuộc hội thoại có attachment. Nếu danh sách tin nhắn tải được và file tải xuống được, bản sao lưu hoạt động.
Khôi phục với SECRET_KEY_BASE khác sẽ vô hiệu hóa mọi session cookie, khiến tất cả người dùng bị đăng xuất. Khôi phục với các key ACTIVE_RECORD_ENCRYPTION_* khác còn nghiêm trọng hơn: Chatwoot không thể giải mã các column chứa thông tin xác thực của channel và phát sinh ActiveRecord::Encryption::Errors::Decryption. Đó là lý do .env nằm trong danh sách sao lưu.
Cách nâng cấp Chatwoot lên tag mới
Thứ tự thực hiện quan trọng hơn bản thân các lệnh.
- Đọc release notes giữa tag hiện tại và tag đích để tìm các bước thủ công bắt buộc.
- Tạo database dump và storage archive mới, rồi kiểm tra kích thước của cả hai file có hợp lý không.
- Sửa image tag của service
basetrongdocker-compose.yaml. - Pull image mới, dừng stack, chạy migration rồi khởi động lại.
docker compose pull
docker compose down
docker compose run --rm rails bundle exec rails db:chatwoot_prepare
docker compose up -d
docker compose imagesHãy pull image trước khi migrate, vì migration phải chạy từ image mới. Image cũ không chứa các migration file mới. Hãy dừng stack trước khi migrate, vì code cũ và schema mới không tương thích. Một Rails process cũ đang chạy có thể phát sinh lỗi hoặc ghi các row mà schema mới không chấp nhận. Việc dừng stack cũng giải phóng lượng memory mà migration cần. Đây là lý do upstream yêu cầu có swap.
docker compose images in ra tag mà mỗi container thực sự đang chạy. Lệnh này giúp phát hiện trường hợp bạn đã sửa tag nhưng quên pull image.
Không nên bỏ qua nhiều version cùng lúc. Với một install cũ, upstream khuyên nên đi qua các tag trung gian, vì migration sẽ bị xóa sau khi được hợp nhất vào base schema. Do đó, một database quá cũ có thể rơi vào trạng thái không còn đường nâng cấp tiếp. Hãy nâng từng minor version một và chạy bước prepare sau mỗi lần nâng cấp.
Nếu Rails khởi động trước khi migration chạy, nó sẽ từ chối phục vụ và ghi ActiveRecord::PendingMigrationError: Migrations are pending vào log. Khi đã đặt restart: always, container sẽ khởi động lại liên tục, nên docker compose ps hiển thị uptime bị reset sau vài giây. Hãy chạy bước prepare để xử lý lỗi này.
Rollback nghĩa là đặt lại tag cũ và khôi phục dump. Không có reverse migration path đáng tin cậy, nên bước 2 là cần thiết.
Các dạng lỗi và chuỗi bạn sẽ thấy
Traefik trả về 502 Bad Gateway. Router đã khớp nhưng backend không phản hồi. Kiểm tra docker compose ps cho thấy rails là Up, sau đó chạy docker network inspect proxy và xác nhận container rails xuất hiện trong danh sách container của nó. Container không được attach sẽ vô hình đối với Traefik, nên request khớp với router rồi không đi đến đâu.
Dashboard tải được nhưng phải refresh mới thấy message mới. Websocket đến /cable không đi qua được, hoặc FRONTEND_URL không khớp với địa chỉ trên thanh địa chỉ của browser. Sai khác này có nghĩa là trang cố mở websocket đến một origin khác, nên browser chặn kết nối.
FATAL: password authentication failed for user "postgres". Password trong .env khác với password đã được ghi vào data volume của Postgres. Sửa bằng ALTER USER bên trong container đang chạy, vì chỉnh lại .env sẽ không thay đổi database đã được khởi tạo.
NOAUTH Authentication required. Redis đang chạy với --requirepass nhưng application kết nối mà không có password, nên REDIS_PASSWORD bị thiếu trong .env hoặc không được nạp. Kiểm tra trực tiếp password mà Redis yêu cầu bằng docker compose exec redis sh -c 'redis-cli -a "$REDIS_PASSWORD" ping'; lệnh này phải trả về PONG. Dấu nháy đơn rất quan trọng: chúng làm cho variable được expand bên trong container, nơi đọc .env, thay vì trong shell của bạn, nơi variable này thường rỗng.
Container thoát với code 137. Đó là SIGKILL. Trên máy có ít tài nguyên, nguyên nhân thường là kernel out of memory killer, nhưng docker kill cũng gửi cùng signal đó, nên hãy kiểm tra sudo dmesg -T | grep -i "killed process" trước khi kết luận là do memory. Nếu đúng là do memory, hãy thêm swap, đặt memory limit cho từng service hoặc chuyển sang plan lớn hơn.
FAQ
VPS tự host Chatwoot cần bao nhiêu RAM?
Tính đến tháng 10 năm 2026, upstream yêu cầu tối thiểu 4 GB RAM và 4 CPU core, phù hợp với tối đa 10,000 cuộc hội thoại mỗi ngày; với tối đa 20,000 cuộc hội thoại, cần 8 GB RAM và 8 core. Hãy thêm ít nhất 1 GB swap, vì khi upgrade, hệ thống chạy process Rails thứ hai để áp dụng migration và đây là lúc các máy cấu hình nhỏ hết bộ nhớ. VPS 2 GB vẫn boot và hoạt động được với vài agent, nhưng riêng Sidekiq có thể vượt 1 GB khi có tải. Vì vậy, trong thời gian cao điểm và khi upgrade, hãy dự kiến container bị kill với exit code 137.
Vì sao email reset password của Chatwoot không bao giờ đến?
Vì chưa đặt SMTP_ADDRESS, nên Chatwoot chuyển sang cơ chế gửi qua sendmail. upstream thiết kế cơ chế này cho Postfix server được cài trên host, nhưng Docker setup này không có Postfix. Job chạy trong Sidekiq, nên mọi lỗi gửi email sẽ xuất hiện trong Sidekiq log, trong khi browser vẫn hiển thị thông báo thành công. Hãy đặt SMTP_ADDRESS, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD và MAILER_SENDER_EMAIL trong .env, restart các service rails và sidekiq, sau đó monitor docker compose logs -f sidekiq trong khi bạn thực hiện reset.
Cần backup những gì để restore Chatwoot?
Database Postgres, Docker volume storage_data, file .env và các compose file. Chỉ backup database là không đủ, vì file upload nằm trong volume còn Postgres chỉ lưu reference đến các file đó. Do đó, restore chỉ database sẽ tạo ra các cuộc hội thoại có attachment bị hỏng. .env quan trọng vì SECRET_KEY_BASE khác sẽ đăng xuất toàn bộ user, còn các key ACTIVE_RECORD_ENCRYPTION_* khác sẽ khiến những column đã mã hóa không thể đọc được.
Làm cách nào để upgrade Chatwoot mà không làm hỏng database?
Hãy backup trước, đổi image tag trong compose file, sau đó chạy docker compose pull, docker compose down, docker compose run --rm rails bundle exec rails db:chatwoot_prepare và docker compose up -d. Hãy pull trước, vì migration phải chạy từ image mới. Đồng thời, hãy stop stack trước, vì code cũ chạy trên schema mới sẽ phát sinh lỗi. Với install cũ, hãy nâng từng minor version một, vì migration sẽ bị xóa sau khi được gộp vào base schema.
Có thể dùng image postgres tiêu chuẩn thay cho pgvector không?
Không. Schema của Chatwoot bật extension vector, nên image postgres mặc định sẽ fail trong lúc chạy db:chatwoot_prepare với lỗi ERROR: extension "vector" is not available, vì image đó không có control file của extension. Hãy giữ pgvector/pgvector:pg16 từ upstream compose file, hoặc dùng image khác có pgvector cho major version Postgres của bạn.