Chatwoot is an open-source customer engagement platform unifying live chat, email, and social/messaging conversations into a shared inbox, with real-time visitor tracking and team collaboration. This guide deploys it via Docker Compose with PostgreSQL, Redis, Sidekiq for background jobs, and Traefik for TLS, then walks through creating and testing a live-chat inbox.
Prerequisites: a Linux server (4 vCPU / 8GB RAM minimum), Docker + Docker Compose, a domain A record (e.g.
chatwoot.example.com), optional SMTP credentials (needed for email channel, password resets, notifications).
Set Up the Project
$ mkdir -p ~/chatwoot/{postgres,redis,storage,letsencrypt}
$ cd ~/chatwoot
-
postgres— database files -
redis— cache + background job queue data -
storage— file uploads/attachments -
letsencrypt— Traefik ACME certs
Generate a secret key:
$ openssl rand -hex 32
Create the environment file:
$ nano .env
DOMAIN=chatwoot.example.com
LETSENCRYPT_EMAIL=admin@example.com
FRONTEND_URL=https://chatwoot.example.com
SECRET_KEY_BASE=GENERATED_SECRET_KEY
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
POSTGRES_DATABASE=chatwoot
POSTGRES_USERNAME=chatwoot
POSTGRES_PASSWORD=STRONG_POSTGRES_PASSWORD
REDIS_URL=redis://redis:6379
RAILS_ENV=production
NODE_ENV=production
RAILS_LOG_TO_STDOUT=true
ACTIVE_STORAGE_SERVICE=local
# Optional: SMTP configuration for email
# SMTP_ADDRESS=smtp.example.com
# SMTP_PORT=587
# SMTP_USERNAME=SMTP_USERNAME
# SMTP_PASSWORD=SMTP_PASSWORD
# SMTP_DOMAIN=chatwoot.example.com
# SMTP_ENABLE_STARTTLS_AUTO=true
# MAILER_SENDER_EMAIL=noreply@chatwoot.example.com
Replace the placeholders with your domain, email, DB password, and generated secret key. Uncomment and fill in the SMTP block if you need email channel support.
Deploy with Docker Compose
$ nano docker-compose.yml
services:
traefik:
image: traefik:v3.7.0
container_name: traefik
restart: unless-stopped
command:
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entrypoints.web.http.redirections.entrypoint.scheme=https"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
- "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.letsencrypt.acme.email=${LETSENCRYPT_EMAIL}"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
volumes:
- "./letsencrypt:/letsencrypt"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
postgres:
image: pgvector/pgvector:pg16
container_name: chatwoot-postgres
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DATABASE}
POSTGRES_USER: ${POSTGRES_USERNAME}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- "./postgres:/var/lib/postgresql/data"
healthcheck:
test: ["CMD", "pg_isready", "-d", "${POSTGRES_DATABASE}", "-U", "${POSTGRES_USERNAME}"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:alpine
container_name: chatwoot-redis
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
volumes:
- "./redis:/data"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
rails:
image: chatwoot/chatwoot:v4.14.1
container_name: chatwoot-rails
restart: unless-stopped
command: bundle exec rails s -p 3000 -b 0.0.0.0
env_file:
- .env
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- "./storage:/app/storage"
labels:
- "traefik.enable=true"
- "traefik.http.routers.chatwoot.rule=Host(`${DOMAIN}`)"
- "traefik.http.routers.chatwoot.entrypoints=websecure"
- "traefik.http.routers.chatwoot.tls.certresolver=letsencrypt"
- "traefik.http.services.chatwoot.loadbalancer.server.port=3000"
sidekiq:
image: chatwoot/chatwoot:v4.14.1
container_name: chatwoot-sidekiq
restart: unless-stopped
command: bundle exec sidekiq -C config/sidekiq.yml
env_file:
- .env
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
volumes:
- "./storage:/app/storage"
- traefik — TLS termination, auto HTTP→HTTPS redirect, Let's Encrypt via ACME
- postgres — primary datastore for conversations/users/config
- redis — caching, sessions, Sidekiq queues
- rails — the web app + WebSocket server on port 3000
- sidekiq — background workers (email, webhooks, scheduled tasks)
Prepare the database and start everything:
$ docker compose run --rm rails bundle exec rails db:chatwoot_prepare
$ docker compose up -d
$ docker compose ps -a
$ docker compose logs
Confirm all containers show Up, and rails/sidekiq/traefik logs show no repeated errors or connection refusals.
First-Run Setup
Visit https://chatwoot.example.com (a 502 on first load just means give it a few seconds and refresh):
- Enter Name, Company Name, Work Email, Password.
- Toggle the newsletter checkbox as you like.
- Finish Setup to land on the dashboard.
Create a Live Chat Inbox
- Click here to create an inbox on the welcome dashboard.
- Select Website as the channel.
- Enter Website Name and Website Domain.
- Set widget color, welcome heading, and tagline; optionally enable channel greeting.
- Create inbox, optionally assign agents, then Copy the embed snippet from the confirmation screen.
Test the Widget
- Settings → Inboxes, click the gear icon on your inbox, then Script to view the embed code.
- Open in CodePen for a live preview.
- Click the chat bubble, send a test message.
- Back in Chatwoot, Conversations shows the new message — open it and reply from the dashboard.
Next Steps
Chatwoot is running with persistent storage, background workers, and TLS. From here:
- Add email and social channels (Facebook, WhatsApp, Instagram) alongside the website widget
- Configure SMTP so password resets and notification emails actually send
- Set up canned responses and team assignment rules to route conversations efficiently
For the full guide, visit the original article on Vultr Docs.
Top comments (1)
I noticed that the
docker-compose.ymlfile uses thepgvector/pgvector:pg16image for PostgreSQL, which is a great choice for efficient full-text search capabilities. However, it's worth considering the implications of using a specialized PostgreSQL image, as it may require additional configuration and tuning for optimal performance. Have you considered using a more standard PostgreSQL image, such as the officialpostgres:16image, and configuring full-text search capabilities separately? This could potentially simplify the deployment process and make it easier to manage and maintain the database.