วิธีติดตั้ง Chatwoot บน VPS ด้วย Docker และ Traefik
เรียนรู้วิธีติดตั้ง Chatwoot บน VPS ด้วย Docker Compose และ Traefik พร้อมการตั้งค่า SMTP การสำรองข้อมูล PostgreSQL และขั้นตอนการอัปเกรดเวอร์ชันอย่างปลอดภัยเพื่อใช้งานจริง
สิ่งที่คุณกำลังสร้าง
ในการ self-host Chatwoot บน VPS คุณต้องรันคอนเทนเนอร์ 4 ตัว ได้แก่ กระบวนการ Rails web, Sidekiq background worker, PostgreSQL ที่ติดตั้งส่วนขยาย pgvector และ Redis โดย Chatwoot เป็นซอฟต์แวร์ open source สำหรับงานบริการลูกค้า ซึ่งช่วยให้คุณมีกล่องข้อความทีมแบบรวมศูนย์และ widget แชทบนเว็บไซต์ที่รันอยู่บนเซิร์ฟเวอร์ที่คุณควบคุมเอง การติดตั้งใช้เวลาประมาณ 20 นาที ส่วนงานหลังจากนั้น เช่น การส่งอีเมล, การสำรองข้อมูล, การอัปเกรด และการปรับขนาดทรัพยากร คือปัจจัยที่จะตัดสินว่าระบบของคุณจะยังคงทำงานได้ต่อไปในอีกหนึ่งปีข้างหน้าหรือไม่
คอนเทนเนอร์แต่ละตัวมีหน้าที่เฉพาะตัว Rails ทำหน้าที่ให้บริการแดชบอร์ดสำหรับเจ้าหน้าที่และ API (application programming interface) ของ widget ส่วน Sidekiq ทำหน้าที่ประมวลผลงานที่ใช้เวลานาน เช่น การส่งอีเมล, การดึงข้อมูลจากช่องทางที่เชื่อมต่อ, การรันกฎอัตโนมัติ และการสร้างรายงาน ทางด้าน Postgres จะเก็บข้อมูลบทสนทนา, ข้อมูลติดต่อ, บัญชีเจ้าหน้าที่ และการตั้งค่าทุกอย่างที่คุณปรับเปลี่ยนในแดชบอร์ด ส่วน Redis จะเก็บคิวของ Sidekiq และช่องทาง ActionCable pub/sub ซึ่งทำหน้าที่ผลักข้อความใหม่เข้าสู่แดชบอร์ดที่เปิดค้างไว้โดยไม่ต้องรีโหลดหน้าเว็บ ในกรณีนี้ Redis ไม่ใช่แค่ cache ทั่วไป เพราะหากข้อมูลสูญหายจะส่งผลให้งานที่อยู่ในคิวสูญหายไปด้วย
อิมเมจ Postgres ในไฟล์ compose ต้นฉบับคือ pgvector/pgvector:pg16 แทนที่จะเป็นอิมเมจ postgres มาตรฐาน เนื่องจาก schema ของ Chatwoot จำเป็นต้องเปิดใช้งานส่วนขยาย vector สำหรับฟีเจอร์ AI หากคุณเปลี่ยนไปใช้ Postgres มาตรฐาน การรันฐานข้อมูลครั้งแรกจะหยุดทำงานพร้อมกับข้อผิดพลาด ERROR: extension "vector" is not available เนื่องจากไฟล์ควบคุมของส่วนขยายดังกล่าวไม่มีอยู่ในอิมเมจนั้น ดังนั้นควรใช้อิมเมจที่ทางต้นฉบับจัดเตรียมไว้ให้
คู่มือนี้ตั้งสมมติฐานว่าคุณได้ติดตั้ง Docker และ reverse proxy ไว้บนเซิร์ฟเวอร์เรียบร้อยแล้ว หากยังไม่มี ให้เริ่มต้นที่ Docker Compose บน VPS แล้วจึงกลับมาดำเนินการต่อ
Chatwoot ที่โฮสต์เองต้องการ VPS สเปกเท่าใด
ณ เดือนสิงหาคม 2026 หน้าเอกสารความต้องการของต้นทางระบุว่าต้องการ RAM ขั้นต่ำ 4 GB และ CPU 4 คอร์ ซึ่งรองรับการสนทนาได้สูงสุด 10,000 ครั้งต่อวัน และระบุว่า 8 GB กับ 8 คอร์ รองรับได้สูงสุด 20,000 ครั้งต่อวัน นอกจากนี้ยังต้องการ swap อย่างน้อย 1 GB โดยให้เหตุผลโดยตรงว่าเพื่อป้องกันไม่ให้เครื่องหน่วยความจำเต็มระหว่างการอัปเกรด ควรเผื่อพื้นที่ดิสก์สำหรับ Postgres ไว้ 5 GB ถึง 10 GB ก่อนนับรวมไฟล์ที่อัปโหลด
เข้าประเด็นตรงไปตรงมาคือ VPS ขนาด 2 GB สามารถบูต Chatwoot ได้ และดูเหมือนจะทำงานได้ปกติหากมีเจ้าหน้าที่ 2 คนและกล่องข้อความที่ไม่มีการใช้งานมากนัก แต่ระบบจะล้มเหลวใน 2 จุด จุดแรกคือ Sidekiq ซึ่งต้นทางวัดค่าการใช้งานไว้ที่กว่า 1 GB บนเซิร์ฟเวอร์ที่มีงานชุก ดังนั้นหากมีอีเมลจำนวนมากหรือการประมวลผลรายงานเข้ามาพร้อมกัน จะทำให้เครื่องใช้หน่วยความจำเกินขีดจำกัดก่อนที่ Rails, Postgres และ Redis จะได้ใช้ส่วนแบ่งของตน จุดที่สองคือการอัปเกรด เนื่องจาก db:chatwoot_prepare จะบูตกระบวนการ Rails ใหม่เพื่อรันการย้ายฐานข้อมูล (migrations) และการบูต Rails บนอิมเมจนี้ใช้หน่วยความจำหลายร้อยเมกะไบต์ก่อนที่จะเริ่มทำงานจริง
ระบบจะไม่มีการแจ้งเตือนล่วงหน้า กลไก out of memory killer ของ kernel จะส่ง SIGKILL ไปยังกระบวนการที่ใช้หน่วยความจำมากที่สุด Docker จะเห็นว่าคอนเทนเนอร์หยุดทำงาน และ restart: always จะเริ่มการทำงานใหม่ docker compose ps จะแสดงคอนเทนเนอร์ที่วนกลับไปที่สถานะ Exited (137) โดยรหัส 137 หมายถึงถูกสั่งหยุดด้วย signal 9 ให้ยืนยันด้วย sudo dmesg -T | grep -i "killed process" ซึ่งจะระบุชื่อกระบวนการที่ kernel เลือกให้หยุดทำงาน
หากงบประมาณไม่เอื้อให้ใช้ 4 GB ให้ใช้เครื่องขนาด 2 GB ร่วมกับ swap 2 GB และยอมรับว่าเวลาในการตอบสนองจะช้าลงเมื่อมีโหลดสูง แทนที่จะให้เซอร์วิสหยุดทำงานไปเลย การกำหนดเพดานหน่วยความจำสูงสุดต่อเซอร์วิสเป็นสิ่งที่ควรทำในทุกกรณี เพื่อไม่ให้ worker ดึงฐานข้อมูลให้ล่มไปด้วย ดูรายละเอียดได้ที่ การจำกัดหน่วยความจำใน Docker Compose
การอัปโหลดไฟล์เป็นส่วนที่เพิ่มขึ้นโดยไม่มีขีดจำกัดที่คุณกำหนดไว้ ภาพหน้าจอทุกภาพที่ลูกค้าแนบจะถูกเก็บไว้ใน storage 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/storageการใช้ latest หมายความว่าคำสั่ง docker compose pull ครั้งถัดไปจะดึงเวอร์ชันล่าสุดที่เผยแพร่ในเช้าวันนั้นมาให้ ซึ่งอาจเป็นเวอร์ชันหลักที่มีการเปลี่ยนแปลงโครงสร้างฐานข้อมูล (migration) ที่คุณไม่ได้อ่านรายละเอียดมาก่อน การทำ migration ของ Chatwoot ในทางปฏิบัติไม่สามารถย้อนกลับได้ ดังนั้นหากข้ามเวอร์ชันโดยไม่ตั้งใจ คุณจะต้องกู้คืนจากข้อมูลสำรองเท่านั้น ไม่ใช่การยกเลิกการทำงาน ให้กำหนดเวอร์ชันแบบเจาะจงและเปลี่ยนด้วยความตั้งใจ v4.16.2 คือเวอร์ชันล่าสุด ณ เดือนสิงหาคม 2026 โปรดตรวจสอบ หน้า releases เพื่อดูแท็กที่คุณควรใช้ในปัจจุบัน
บริการ base เป็น YAML anchor ที่ทั้ง rails และ sidekiq ใช้งานร่วมกัน ดังนั้นการเปลี่ยนแท็กในจุดเดียวจะส่งผลกับทั้งสองบริการ ในขณะที่คุณอยู่ในไฟล์ ให้ลบบรรทัด version: '3' ที่ด้านบนออก เนื่องจาก Docker Compose เวอร์ชันปัจจุบันไม่สนใจบรรทัดนี้และจะแสดงข้อความ the attribute 'version' is obsolete, it will be ignored ทุกครั้งที่เรียกใช้คำสั่ง
กรอกข้อมูลในไฟล์ .env
สร้างค่า secret ขึ้นมาก่อน ต้นทางต้องการค่าที่เป็นตัวอักษรและตัวเลขเท่านั้น เนื่องจากอักขระพิเศษอาจเกิดความผิดพลาดเมื่อค่าถูกส่งผ่าน 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=localPOSTGRES_HOST=postgres และ redis://redis:6379 คือชื่อบริการของ Compose ซึ่งจะ resolve กันเองบนเครือข่ายเริ่มต้นของโปรเจกต์ FRONTEND_URL ไม่ใช่แค่ส่วนประกอบตกแต่ง Chatwoot จะนำค่านี้ไปสร้าง URL ของสคริปต์ widget และลิงก์ทุกอย่างในอีเมลขาออก หากค่านี้ผิดพลาด ลิงก์รีเซ็ตรหัสผ่านจะชี้ไปยังโฮสต์ที่ไม่ตอบสนอง
จุดที่ต้องระวังในไฟล์ของต้นทางคือ บริการ 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 จะไม่ทำงานซ้ำเป็นครั้งที่สอง หากคุณเริ่ม 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 จะสามารถลงทะเบียนบนระบบ support desk ของคุณได้ หลังจากนั้นตัวแทนจะเข้ามาได้ผ่านการเชิญเท่านั้น และรหัสผ่านของพวกเขาจะอยู่ในแอปนี้แอปเดียว ซึ่งอาจจะไม่มีปัญหาจนกว่าคุณจะเริ่มรันบริการหลายตัวและเบื่อกับการจัดการรายชื่อบัญชีแยกกันในแต่ละที่ ถึงจุดนั้น ผู้ให้บริการยืนยันตัวตนแบบ self-hosted อย่าง Authentik คือสิ่งที่เข้ามาแทนที่การจัดการบัญชีแบบเดิม
ตอนนี้ .env เก็บความลับทั้งหมดของ stack นี้ไว้ในรูปแบบข้อความธรรมดา ดังนั้นควรตั้งค่าสิทธิ์เป็น 600 และไม่นำไฟล์นี้เข้า git วิธีที่ Compose อ่านไฟล์ env และจุดที่ความลับอาจรั่วไหล ครอบคลุมถึงประเด็นสำคัญต่างๆ รวมถึงความแตกต่างระหว่าง env_file และ environment
การติดตั้ง Chatwoot ไว้หลัง Traefik ที่คุณมีอยู่แล้ว
อย่าสร้าง reverse proxy ตัวที่สองสำหรับแอปพลิเคชันเดียว หาก Traefik ทำหน้าที่จัดการ TLS (transport layer security) ให้กับคอนเทนเนอร์อื่นบนเซิร์ฟเวอร์นี้อยู่แล้ว ให้เพิ่ม Chatwoot เข้าไปโดยใช้บล็อก label หากคุณยังไม่มีการตั้งค่าดังกล่าว ให้ดำเนินการตั้งค่าให้เรียบร้อยก่อนโดยดูที่ Traefik หน้าแอป Docker Compose หลายตัว แล้วจึงกลับมาที่นี่
รักษาไฟล์ docker-compose.yaml ของต้นฉบับให้ใกล้เคียงกับค่าเริ่มต้นมากที่สุด เพื่อให้คุณสามารถเปรียบเทียบความแตกต่าง (diff) กับเวอร์ชันใหม่ในอนาคตได้ง่าย และให้ใส่การตั้งค่าที่คุณปรับเปลี่ยนไว้ในไฟล์ override แทน Docker 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 ของคุณเอง คอนเทนเนอร์ต้องอยู่ใน Docker network เดียวกับ Traefik ซึ่งเป็นสิ่งที่รายการ proxy ทำหน้าที่จัดการ และต้องคงไว้ใน default ด้วยเช่นกัน มิฉะนั้นคอนเทนเนอร์จะสูญเสียการเชื่อมต่อกับ Postgres และ Redis ซึ่งเป็นบรรทัดที่ผู้ใช้งานมักลืมตั้งค่า
ปล่อยบล็อก ports: ไว้ตามเดิม ต้นฉบับกำหนดให้ผูกกับ 127.0.0.1:3000 ซึ่งเป็น loopback เท่านั้น จึงไม่สามารถเข้าถึงได้จากอินเทอร์เน็ต แต่ยังคงมีประโยชน์สำหรับการทดสอบจากภายในเซิร์ฟเวอร์ด้วยคำสั่ง curl -I http://127.0.0.1:3000
แดชบอร์ดของเจ้าหน้าที่จะเปิดการเชื่อมต่อ websocket ค้างไว้ที่ /cable เพื่อรับข้อความแบบเรียลไทม์ Traefik จะส่งต่อการร้องขอ HTTP upgrade โดยอัตโนมัติโดยไม่ต้องตั้งค่าเพิ่มเติม ดังนั้นจึงไม่มีอะไรต้องเพิ่ม หากในอนาคตคุณนำ CDN หรือ proxy อื่นมาวางไว้หน้า Traefik อีกชั้นหนึ่ง โปรดอนุญาตให้มีการใช้งาน websockets ที่นั่นด้วย เพราะหากไม่ได้ตั้งค่า อาการที่พบคือแดชบอร์ดจะโหลดข้อมูลได้ตามปกติ แต่ข้อความใหม่จะปรากฏขึ้นก็ต่อเมื่อกดรีเฟรชหน้าเว็บด้วยตนเองเท่านั้น
การเตรียมฐานข้อมูลและเริ่มการทำงานของ stack
ให้เริ่มการทำงานของบริการข้อมูลก่อน และปล่อยให้ Postgres ทำงานรอบแรกให้เสร็จสิ้น
docker compose up -d postgres redis
docker compose logs postgres | tail -n 5รอจนกว่า database system is ready to accept connections จะพร้อม จากนั้นจึงสร้าง schema
docker compose run --rm rails bundle exec rails db:chatwoot_prepareคำสั่งนี้จะสร้างฐานข้อมูลหากยังไม่มีอยู่ จากนั้นจะโหลด schema และข้อมูลตั้งต้น (seed data) ระบบจะแสดงรายการ migration และจบการทำงานอย่างปกติ หากระบบค้างอยู่ที่การแสดง postgres:5432 - no response แสดงว่า entrypoint กำลังรอฐานข้อมูลที่ยังไม่พร้อมรับการเชื่อมต่อ ซึ่งในการรันครั้งแรกมักหมายความว่า initdb กำลังทำงานอยู่ ให้รอ ตรวจสอบ log ของ Postgres แล้วจึงรันคำสั่งอีกครั้ง หากระบบหยุดทำงานที่ส่วนขยาย vector แสดงว่าคุณได้แทนที่ image ของ pgvector ด้วย Postgres รุ่นมาตรฐาน
docker compose up -d
docker compose ps
docker compose logs --tail 30 railsคอนเทนเนอร์ทั้งสี่ควรแสดงสถานะ Up และ log ของ rails ควรจบด้วยบรรทัดของ Puma ที่ระบุว่ากำลังฟังอยู่ที่ http://0.0.0.0:3000 จากนั้นให้ตรวจสอบ public path:
curl -sI https://support.example.com | head -n 1HTTP/2 200 หมายความว่าห่วงโซ่การทำงานทั้งหมดใช้งานได้ปกติ หากได้รับ 404 จาก Traefik แสดงว่ากฎของ router ไม่ตรงกัน ซึ่งมักเกิดจากการพิมพ์ชื่อ host ผิด หากได้รับ 502 แสดงว่า Traefik พบ router แล้วแต่ไม่สามารถเชื่อมต่อไปยังคอนเทนเนอร์ได้ ซึ่งมักเกิดจากการขาด network proxy หรือค่า loadbalancer.server.port ไม่ใช่ 3000
เปิด URL สร้างบัญชีของคุณที่ /app/auth/signup จากนั้นตั้งค่า ENABLE_ACCOUNT_SIGNUP=false และรัน docker compose up -d เพื่อปิดฟอร์มดังกล่าว
เหตุใดการรีเซ็ตรหัสผ่านและการสนทนาทางอีเมลจึงล้มเหลวหากไม่มี SMTP
Chatwoot ที่ไม่มีการตั้งค่า SMTP (Simple Mail Transfer Protocol) จะเป็นระบบสนับสนุนที่ไม่สามารถส่งอีเมลได้ ซึ่งส่งผลกระทบมากกว่าแค่เรื่องการแจ้งเตือน การรีเซ็ตรหัสผ่านจะใช้งานไม่ได้ ทำให้ผู้ดูแลระบบที่ถูกล็อกออกจากระบบไม่สามารถเข้าใช้งานได้อีกต่อไป การเชิญเจ้าหน้าที่เข้าสู่ระบบก็ทำไม่ได้เช่นกันเนื่องจากการเชิญต้องส่งผ่านอีเมล การตอบกลับลูกค้าในการสนทนาทางอีเมลก็ทำไม่ได้ ทำให้การสนทนาเกิดขึ้นได้เพียงทางเดียว นี่คือขั้นตอนที่ผู้คนมักข้ามไปและมาพบปัญหาในภายหลังในช่วงเวลาที่วิกฤตที่สุด
กลไกของเรื่องนี้ชัดเจน เมื่อไม่มีการตั้งค่า SMTP ตัว ActionMailer จะคงค่าเริ่มต้นในการส่งไปยัง localhost ที่พอร์ต 25 เนื่องจากไม่มีเมลเซิร์ฟเวอร์อยู่ภายในคอนเทนเนอร์ Rails งานการส่งอีเมลจึงแจ้งข้อผิดพลาด Errno::ECONNREFUSED: Connection refused - connect(2) for "localhost" port 25 ออกมา อีเมลถูกส่งออกจากงานเบื้องหลัง (background job) บรรทัดข้อผิดพลาดจึงไปปรากฏใน log ของ Sidekiq แทนที่จะเป็น log ของ 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 ขาออกเพื่อจำกัดสแปม ดังนั้นการใช้ relay บนพอร์ต 587 จึงมักเป็นวิธีเดียวที่สามารถเชื่อมต่อได้ SMTP_DOMAIN คือโดเมนที่เซิร์ฟเวอร์ของคุณประกาศระหว่างการสนทนา SMTP และ relay บางแห่งจะปฏิเสธหากข้อมูลไม่ตรงกัน
ใช้การตั้งค่าและตรวจสอบการทำงานของ worker:
docker compose up -d rails sidekiq
docker compose logs -f sidekiqทดสอบการรีเซ็ตรหัสผ่านจากหน้าล็อกอิน หากการส่งอีเมลทำงานได้ปกติ งาน mailer จะแสดงสถานะเสร็จสิ้นใน log ของ Sidekiq หากล้มเหลวจะแสดงคลาสของข้อยกเว้น จากนั้น Sidekiq จะพยายามส่งใหม่โดยเพิ่มระยะเวลาถอยหลัง (backoff) ซึ่งเป็นเหตุผลว่าทำไม relay ที่มีปัญหาจึงแสดงข้อผิดพลาดเดิมซ้ำๆ ทุกสองสามนาทีเป็นเวลาหลายชั่วโมง
การถูกปฏิเสธมีอยู่ 2 กรณีที่พบบ่อย ซึ่งไม่ใช่บั๊กของ Chatwoot แต่อย่างใด 535 Authentication failed หมายความว่าชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้องสำหรับ relay นั้นๆ และผู้ให้บริการหลายรายต้องการ application password แทนรหัสผ่านบัญชีปกติ ส่วน 550 Sender address rejected หมายความว่า MAILER_SENDER_EMAIL เป็นที่อยู่ที่ relay ไม่อนุญาตให้ใช้ส่งอีเมล ดังนั้นที่อยู่นั้นต้องเป็นกล่องจดหมายหรือโดเมนที่คุณได้ยืนยันกับผู้ให้บริการแล้ว
การรับอีเมลเข้าสู่การสนทนาเป็นอีกงานหนึ่งที่แยกต่างหาก ซึ่งจำเป็นต้องใช้ MAILER_INBOUND_EMAIL_DOMAIN และ RAILS_INBOUND_EMAIL_SERVICE รวมถึงเมลเซิร์ฟเวอร์ที่ทำหน้าที่ส่งข้อความขาเข้าให้กับ Chatwoot การเช่าบริการ relay เป็นวิธีที่รวดเร็วที่สุด หากคุณต้องการดูแลเส้นทางอีเมลทั้งหมดด้วยตนเอง การรันเมลเซิร์ฟเวอร์ของคุณเองด้วย Mailcow จะอธิบายถึงสิ่งที่ต้องรับผิดชอบอย่างแท้จริงในกรณีนี้
สิ่งที่ต้องสำรองข้อมูลและวิธีตรวจสอบว่าการกู้คืนใช้งานได้จริง
การสำรองข้อมูล Chatwoot ประกอบด้วยสี่ส่วน หากขาดส่วนใดส่วนหนึ่งไป การกู้คืนจะกลายเป็นการสร้างระบบใหม่แทน
- ฐานข้อมูล Postgres ซึ่งเก็บข้อมูลบทสนทนา รายชื่อผู้ติดต่อ บัญชีเจ้าหน้าที่ และการตั้งค่าทั้งหมด
- วอลลุ่ม
storage_dataเนื่องจากACTIVE_STORAGE_SERVICE=localจะเขียนไฟล์ที่อัปโหลดลงในดิสก์และเก็บเพียงอ้างอิงไว้ใน Postgres เท่านั้น - ไฟล์
.envเนื่องจากเป็นที่เก็บSECRET_KEY_BASEและกุญแจACTIVE_RECORD_ENCRYPTION_* - ไฟล์ compose เนื่องจากเป็นตัวบันทึก image tag ที่ตรงกับ schema ของฐานข้อมูลของคุณ
หากกู้คืนเฉพาะฐานข้อมูล บทสนทนาทั้งหมดจะกลับมาพร้อมกับไฟล์แนบที่ใช้งานไม่ได้ เนื่องจากแถวข้อมูลชี้ไปยังไฟล์ที่ไม่มีอยู่บนดิสก์แล้ว
cd ~/chatwoot
docker compose exec -T postgres pg_dump -U postgres -Fc chatwoot > db-$(date +%F).dump-T มีความสำคัญ หากไม่มีแฟล็กนี้ Compose จะจัดสรร pseudo terminal ซึ่งจะเขียนทับไบต์ขึ้นบรรทัดใหม่ในสตรีม ทำให้ได้ไฟล์ดัมพ์ที่ 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 ที่ต่างไปจะทำให้ session cookie ทั้งหมดเป็นโมฆะ ส่งผลให้ทุกคนถูกออกจากระบบ การกู้คืนด้วยกุญแจ ACTIVE_RECORD_ENCRYPTION_* ที่ต่างไปจะเลวร้ายยิ่งกว่า: Chatwoot จะไม่สามารถถอดรหัสคอลัมน์ที่เก็บข้อมูลรับรองของช่องทางสื่อสารได้และจะแสดงข้อผิดพลาด ActiveRecord::Encryption::Errors::Decryption นี่คือเหตุผลว่าทำไม .env จึงอยู่ในรายการสำรองข้อมูล
วิธีการอัปเกรด Chatwoot ไปยังแท็กใหม่
ลำดับขั้นตอนมีความสำคัญมากกว่าตัวคำสั่ง
- อ่านบันทึกประจำรุ่น (release notes) ระหว่างแท็กปัจจุบันของคุณกับแท็กเป้าหมาย เพื่อตรวจสอบว่ามีขั้นตอนที่ต้องทำด้วยตนเองหรือไม่
- สำรองข้อมูลฐานข้อมูล (database dump) และคลังจัดเก็บข้อมูล (storage archive) ชุดใหม่ พร้อมตรวจสอบว่าขนาดไฟล์ทั้งสองดูเป็นปกติ
- แก้ไข image tag ของบริการ
baseในไฟล์docker-compose.yaml - ดึงอิมเมจใหม่ (pull), หยุดการทำงานของ 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ให้ดึงอิมเมจใหม่ก่อนเริ่มการย้ายข้อมูล เพราะการย้ายข้อมูลต้องรันจากอิมเมจใหม่เท่านั้น เนื่องจากอิมเมจเก่าไม่มีไฟล์สำหรับการย้ายข้อมูลเวอร์ชันใหม่ ให้หยุดการทำงานของ stack ก่อนเริ่มการย้ายข้อมูล เพราะโค้ดเก่ากับ schema ใหม่ไม่สอดคล้องกัน กระบวนการ Rails เวอร์ชันเก่าที่ยังทำงานอยู่อาจทำให้เกิดข้อผิดพลาดหรือเขียนข้อมูลลงในแถวที่ schema ใหม่ไม่รองรับ การหยุดการทำงานยังช่วยคืนหน่วยความจำที่จำเป็นสำหรับการย้ายข้อมูล ซึ่งเป็นเหตุผลหลักที่ผู้พัฒนาต้นทางแนะนำให้มี swap
docker compose images จะแสดงแท็กที่แต่ละคอนเทนเนอร์กำลังใช้งานจริง ซึ่งช่วยตรวจสอบกรณีที่คุณแก้ไขแท็กแล้วลืมดึงอิมเมจใหม่
อย่าข้ามเวอร์ชันหลายเวอร์ชันในคราวเดียว คำแนะนำจากผู้พัฒนาต้นทางสำหรับการติดตั้งเวอร์ชันเก่าคือให้ค่อยๆ อัปเกรดผ่านแท็กกลางไปทีละขั้น เนื่องจากการย้ายข้อมูลจะถูกลบออกเมื่อถูกรวมเข้ากับ base schema แล้ว ดังนั้นฐานข้อมูลที่เก่ามากอาจเข้าสู่สถานะที่ไม่สามารถอัปเกรดต่อได้ ให้ขยับทีละ minor version และรันขั้นตอนการเตรียมการ (prepare step) หลังจากแต่ละครั้ง
หาก Rails เริ่มทำงานก่อนที่การย้ายข้อมูลจะเสร็จสิ้น ระบบจะปฏิเสธการให้บริการและบันทึก ActiveRecord::PendingMigrationError: Migrations are pending ลงใน log หากตั้งค่า restart: always ไว้ คอนเทนเนอร์จะวนลูปการทำงาน ส่งผลให้ docker compose ps แสดงค่า uptime ที่รีเซ็ตใหม่ทุกสองสามวินาที ให้รันขั้นตอนการเตรียมการเพื่อแก้ไขปัญหานี้
การย้อนกลับ (rollback) หมายถึงการเปลี่ยนกลับไปใช้แท็กเดิมและกู้คืนข้อมูลจากไฟล์สำรอง ไม่มีเส้นทางการย้ายข้อมูลย้อนกลับที่คุณสามารถเชื่อถือได้ ซึ่งเป็นเหตุผลว่าทำไมขั้นตอนที่ 2 จึงมีความสำคัญ
รูปแบบความล้มเหลวและข้อความที่คุณจะพบ
502 Bad Gateway จาก Traefik ตัว router จับคู่คำขอได้สำเร็จแต่ backend ไม่ตอบสนอง ให้ตรวจสอบ docker compose ps ว่าแสดงค่า rails เป็น Up หรือไม่ จากนั้นรัน docker network inspect proxy เพื่อยืนยันว่า container ของ rails ปรากฏอยู่ในรายการ container หาก container ไม่ได้ทำงานอยู่ Traefik จะมองไม่เห็น ทำให้คำขอที่จับคู่กับ router ได้นั้นไม่มีปลายทางให้ส่งไป
Dashboard โหลดได้แต่ข้อความใหม่ต้องกดรีเฟรช การเชื่อมต่อ websocket ไปยัง /cable ไม่สามารถทำได้ หรือ FRONTEND_URL ไม่ตรงกับที่อยู่บนแถบที่อยู่ของเบราว์เซอร์ การที่ค่าไม่ตรงกันทำให้หน้าเว็บพยายามเปิด websocket ไปยัง origin อื่น ซึ่งเบราว์เซอร์จะบล็อกการเชื่อมต่อนี้
FATAL: password authentication failed for user "postgres" รหัสผ่านใน .env ไม่ตรงกับรหัสผ่านที่ถูกกำหนดไว้ใน Postgres data volume ให้แก้ไขด้วย ALTER USER ภายใน container ที่กำลังทำงานอยู่ เนื่องจากการแก้ไข .env อีกครั้งจะไม่ส่งผลต่อฐานข้อมูลที่ถูก initialised ไปแล้ว
NOAUTH Authentication required. Redis ทำงานโดยเปิดใช้งาน --requirepass แต่แอปพลิเคชันเชื่อมต่อโดยไม่มีรหัสผ่าน แสดงว่า REDIS_PASSWORD หายไปจาก .env หรือระบบไม่ได้โหลดค่าดังกล่าว ให้ทดสอบโดยตรงด้วย docker compose exec redis redis-cli -a "$REDIS_PASSWORD" ping ซึ่งควรตอบกลับมาเป็น PONG
Container หยุดทำงานด้วย exit code 137 นี่คือสัญญาณ SIGKILL ซึ่งบนเซิร์ฟเวอร์ขนาดเล็กหมายถึง kernel สั่งยุติการทำงานเนื่องจากหน่วยความจำไม่เพียงพอ (out of memory killer) ให้เพิ่ม swap, กำหนดขีดจำกัดหน่วยความจำต่อบริการ หรือย้ายไปใช้แผนบริการที่มีทรัพยากรสูงขึ้น
FAQ
Chatwoot ที่โฮสต์เองต้องใช้ RAM เท่าไรบน VPS?
ณ เดือนสิงหาคม 2026 ผู้พัฒนาแนะนำ RAM ขั้นต่ำ 4 GB และ CPU 4 คอร์ สำหรับการรองรับบทสนทนาสูงสุด 10,000 รายการต่อวัน และ RAM 8 GB พร้อม CPU 8 คอร์ สำหรับการรองรับสูงสุด 20,000 รายการ ควรเพิ่ม swap อย่างน้อย 1 GB เพราะในระหว่างการอัปเกรด ระบบจะรันกระบวนการ Rails ตัวที่สองเพื่อปรับปรุงฐานข้อมูล (migrations) ซึ่งเป็นจุดที่เซิร์ฟเวอร์ขนาดเล็กมักประสบปัญหาหน่วยความจำไม่พอ VPS ขนาด 2 GB สามารถบูตและใช้งานได้สำหรับเจ้าหน้าที่จำนวนน้อย แต่เฉพาะ Sidekiq ตัวเดียวก็อาจใช้ RAM เกิน 1 GB ในช่วงที่มีโหลดสูง ดังนั้นให้คาดการณ์ไว้ว่าคอนเทนเนอร์อาจถูกสั่งหยุดทำงานด้วย exit code 137 ในช่วงที่มีการใช้งานหนาแน่นหรือระหว่างการอัปเกรด
ทำไมอีเมลรีเซ็ตรหัสผ่านของ Chatwoot ถึงไม่ส่งมา?
เพราะยังไม่ได้ตั้งค่า SMTP ทำให้ ActionMailer พยายามส่งอีเมลไปยัง localhost ผ่านพอร์ต 25 ซึ่งไม่มีเมลเซิร์ฟเวอร์อยู่ภายในคอนเทนเนอร์ งานดังกล่าวจะล้มเหลวใน Sidekiq ด้วยข้อผิดพลาด Errno::ECONNREFUSED: Connection refused - connect(2) for "localhost" port 25 ในขณะที่เบราว์เซอร์ยังคงแสดงข้อความว่าดำเนินการสำเร็จ ให้ตั้งค่า SMTP_ADDRESS, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD และ MAILER_SENDER_EMAIL ในไฟล์ .env จากนั้นรีสตาร์ทบริการ rails และ sidekiq แล้วตรวจสอบ docker compose logs -f sidekiq ในขณะที่คุณกดรีเซ็ตรหัสผ่าน
ต้องสำรองข้อมูลอะไรบ้างเพื่อกู้คืน Chatwoot?
ฐานข้อมูล Postgres, Docker volume ของ storage_data, ไฟล์ .env และไฟล์ compose ต่างๆ การสำรองเฉพาะฐานข้อมูลไม่เพียงพอ เพราะไฟล์ที่อัปโหลดจะอยู่ใน volume ในขณะที่ Postgres เก็บเพียงข้อมูลอ้างอิงเท่านั้น ดังนั้นการกู้คืนเฉพาะฐานข้อมูลจะทำให้บทสนทนาของคุณมีไฟล์แนบที่เปิดไม่ได้ .env มีความสำคัญเพราะหาก SECRET_KEY_BASE เปลี่ยนไป ระบบจะบังคับให้ผู้ใช้ทุกคนออกจากระบบ และหากคีย์ ACTIVE_RECORD_ENCRYPTION_* เปลี่ยนไป จะทำให้ข้อมูลในคอลัมน์ที่เข้ารหัสไว้ไม่สามารถอ่านได้
จะอัปเกรด Chatwoot อย่างไรไม่ให้ฐานข้อมูลเสียหาย?
ให้สำรองข้อมูลก่อน เปลี่ยน image tag ในไฟล์ compose ของคุณ จากนั้นรัน docker compose pull, docker compose down, docker compose run --rm rails bundle exec rails db:chatwoot_prepare และ docker compose up -d ให้ทำการ pull image ใหม่ก่อนเสมอเพราะการทำ migrations ต้องใช้โค้ดจาก image ใหม่ และต้องหยุด stack ก่อนเริ่มอัปเกรดเพราะโค้ดเวอร์ชันเก่าที่ทำงานกับ schema เวอร์ชันใหม่จะทำให้เกิดข้อผิดพลาด หากเป็นการติดตั้งเวอร์ชันเก่า ให้ค่อยๆ อัปเกรดทีละ minor version เนื่องจากไฟล์ migration มักจะถูกลบออกหลังจากถูกรวมเข้ากับ base schema แล้ว
ฉันสามารถใช้ image มาตรฐานของ postgres แทน pgvector ได้หรือไม่?
ไม่ได้ เพราะ schema ของ Chatwoot จำเป็นต้องเปิดใช้งาน extension vector ดังนั้น image มาตรฐานอย่าง postgres จะล้มเหลวระหว่างขั้นตอน db:chatwoot_prepare ด้วยข้อผิดพลาด ERROR: extension "vector" is not available เนื่องจากไฟล์ควบคุมของ extension ดังกล่าวไม่มีอยู่ใน image นั้น ให้ใช้ pgvector/pgvector:pg16 จากไฟล์ compose ของผู้พัฒนา หรือใช้ image อื่นที่มี pgvector สำหรับ Postgres เวอร์ชันหลักที่คุณใช้งานอยู่เท่านั้น