Self-hosted Chatwoot on a single domain, deployed with Docker Compose behind a Traefik reverse proxy. Based on the official Chatwoot production template.
Internet
│
▼
Traefik (ports 80 / 443, automatic TLS via Let's Encrypt)
│
├── app.chat.yourdomain.com → chatwoot_rails (Rails web server)
└── evo.chat.yourdomain.com → evolution_api (Evolution API)
Shared network (chatwoot-net)
├── chatwoot_sidekiq (Sidekiq background worker)
├── chatwoot_postgres (PostgreSQL 16 + pgvector)
└── chatwoot_redis (Redis)
| Container | Image | Purpose |
|---|---|---|
chatwoot_traefik |
traefik:v3.6 |
Reverse proxy, TLS termination (Let's Encrypt) |
chatwoot_postgres |
pgvector/pgvector:pg16 |
Relational database |
chatwoot_redis |
redis:alpine |
Cache and Sidekiq job queues |
chatwoot_rails |
chatwoot/chatwoot:latest |
Rails web server (runs DB migrations on start) |
chatwoot_sidekiq |
chatwoot/chatwoot:latest |
Sidekiq background worker |
evolution_api |
evoapicloud/evolution-api:v2.3.6 |
WhatsApp gateway (Evolution API) |
Environment files:
| File | Purpose |
|---|---|
.env |
Shared infra: domain, ACME e-mail, Postgres and Redis credentials |
chatwoot.env |
Chatwoot application vars (secrets, DB, Redis, SMTP) |
evolution.env |
Evolution API vars (API key, Chatwoot integration flag) |
| Requirement | Notes |
|---|---|
| Ubuntu 22.04 or 24.04 LTS | Other Linux distros work |
| Docker >= 24 + Docker Compose v2 | curl -fsSL https://get.docker.com | sh |
| A DNS A record for the app subdomain | e.g. app.chat.yourdomain.com -> <server-ip> |
| A DNS A record for the evo subdomain | e.g. evo.chat.yourdomain.com -> <server-ip> |
| Ports 80 and 443 open | Required by Traefik and the ACME HTTP-01 challenge |
git clone https://github.com/santzit/chatwoot.git /opt/chatwoot
cd /opt/chatwootcp .env.example .envEdit .env and fill in every value:
DOMAIN=chat.yourdomain.com # base domain (no protocol prefix)
ACME_EMAIL=you@yourdomain.com # plain e-mail for Let's Encrypt
POSTGRES_USER=chatwoot
POSTGRES_PASSWORD=<strong-password> # openssl rand -hex 32
REDIS_PASSWORD=<strong-password> # openssl rand -hex 32With DOMAIN=chat.yourdomain.com, Traefik will serve Chatwoot at https://app.chat.yourdomain.com.
⚠️ ACME_EMAILmust be a plain address — no display name, no angle brackets.
cp chatwoot.env.example chatwoot.env
chmod 600 chatwoot.envEdit chatwoot.env and fill in every value. Generate the required secrets:
openssl rand -hex 64 # SECRET_KEY_BASE
openssl rand -hex 32 # ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY
openssl rand -hex 32 # ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT
openssl rand -hex 32 # ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEYSet the database and Redis credentials to match .env:
POSTGRES_USERNAME=chatwoot # same as POSTGRES_USER in .env
POSTGRES_PASSWORD=<same-as-env>
REDIS_URL=redis://:<redis-password>@redis:6379/0Fill in the SMTP block so Chatwoot can send e-mails:
SMTP_ADDRESS=smtp.example.com
SMTP_PORT=587
SMTP_USERNAME=user@example.com
SMTP_PASSWORD=<smtp-password>
MAILER_SENDER_EMAIL=support@yourdomain.comℹ️
FRONTEND_URL,NODE_ENV,RAILS_ENV, andINSTALLATION_ENVare set automatically bydocker-compose.yml— do not add them tochatwoot.env.
cp evolution.env.example evolution.env
chmod 600 evolution.envEdit evolution.env and set the API key:
AUTHENTICATION_API_KEY=<strong-random-key> # openssl rand -hex 32Instances are persisted in the evolution_instances Docker volume — no external database or Redis is required. SERVER_URL is injected automatically by docker-compose.yml as https://evo.<DOMAIN>.
ℹ️ The
evo.<DOMAIN>DNS A record must point to this server before starting the stack so Traefik can issue a TLS certificate.
install -m 600 /dev/null acme.jsondocker compose up -dOn the very first deploy, you must initialise the database schema explicitly:
docker compose run --rm rails bundle exec rails db:chatwoot_prepareThis creates all tables, loads the schema, and seeds initial data. It takes 2–4 minutes on a typical VPS. On subsequent restarts the same command runs automatically as part of the container startup sequence, so no manual step is needed after the first deploy.
Follow the startup logs after migrations finish:
docker compose logs -f railsLook for:
* Listening on http://0.0.0.0:3000
Once it appears, open https://app.chat.yourdomain.com in your browser. Traefik issues the TLS certificate automatically via Let's Encrypt (allow up to 60 seconds on first request — DNS must be live).
# All services
docker compose logs -f
# Rails web server
docker compose logs -f rails
# Sidekiq background worker
docker compose logs -f sidekiq
# Evolution API
docker compose logs -f evolution-api
# Postgres / Redis
docker compose logs -f postgres
docker compose logs -f redisdocker exec -it chatwoot_rails bundle exec rails consoledocker compose restart rails
docker compose restart sidekiq
docker compose restart evolution-apidocker compose psPull the latest images and restart:
docker compose pull
docker compose up -ddb:chatwoot_prepare runs automatically as part of the container startup sequence and applies any pending migrations from the new image.
docker compose exec postgres \
pg_dump -U chatwoot -d chatwoot \
| gzip > backups/chatwoot_$(date +%Y%m%d_%H%M%S).sql.gzgunzip -c backups/chatwoot_<timestamp>.sql.gz \
| docker compose exec -T postgres \
psql -U chatwoot -d chatwootdocker run --rm \
-v chatwoot_storage_data:/data \
-v "$(pwd)/backups":/backups \
alpine \
tar czf /backups/chatwoot_storage_$(date +%Y%m%d_%H%M%S).tar.gz -C /data .After running docker compose run --rm rails bundle exec rails db:chatwoot_prepare, the first schema load takes 2–4 minutes. This is normal. Follow the logs with docker compose logs -f rails and wait for Listening on http://0.0.0.0:3000.
The database schema is empty — db:chatwoot_prepare has not been run yet (or failed on a previous attempt).
Run migrations manually (always safe — skips already-applied changes):
docker compose run --rm rails bundle exec rails db:chatwoot_prepare
docker compose restart rails sidekiqIf that still fails, the postgres volume may be in a corrupted state from a previous failed deploy:
docker compose down -v # removes volumes — all data will be lost
docker compose up -d
docker compose run --rm rails bundle exec rails db:chatwoot_prepareACME_EMAIL in .env is empty or has a display-name format. Fix it and clear the stale ACME cache:
nano .env
# Must be a plain address: ACME_EMAIL=you@yourdomain.com
docker compose stop traefik
echo '{}' > acme.json && chmod 600 acme.json
docker compose up -d traefikchmod 600 acme.json
docker compose restart traefikTraefik returns 404 when no router matches. This usually means the rails container is not running or still starting. Check:
docker compose ps
docker compose logs rails| Symptom | Cause | Fix |
|---|---|---|
rails in "Created" or "Exited" state |
A dependency was unhealthy | docker compose up -d |
rails keeps restarting |
Startup error | Check docker compose logs rails |
| Still 404 after rails is healthy | DNS A record not pointing to this server | dig app.chat.yourdomain.com |
On first start, migrations take 3–5 minutes before Rails accepts HTTP connections. If the healthcheck expired before migrations finished, reset it:
docker exec chatwoot_rails nc -z localhost 3000 && echo "PORT OPEN"
# If PORT OPEN, rails is fine — restart to reset the healthcheck state:
docker compose restart rails
docker compose up -d- Confirm DNS has propagated:
dig app.chat.yourdomain.comanddig evo.chat.yourdomain.com - Confirm port 80 is reachable from the internet.
- Check Traefik logs:
docker compose logs traefik | grep -i acme
SMTP is misconfigured. Edit chatwoot.env, update the SMTP_* block, then restart:
docker compose restart rails sidekiq.env,chatwoot.env, andevolution.envare listed in.gitignore— never commit them.acme.jsonis also gitignored — it contains your TLS private keys.- Generate unique cryptographic secrets for each deployment.
- PostgreSQL and Redis have no published ports — accessible only inside
chatwoot-net. - Evolution API has no published ports — exposed only via Traefik at
https://evo.<DOMAIN>. - Traefik only routes containers carrying the
traefik.enable=truelabel.
| Variable | Required | Description |
|---|---|---|
DOMAIN |
✅ | Base domain; Chatwoot served at app.<DOMAIN> (e.g. chat.yourdomain.com) |
ACME_EMAIL |
✅ | Plain e-mail for Let's Encrypt certificate registration |
POSTGRES_USER |
✅ | PostgreSQL superuser name |
POSTGRES_PASSWORD |
✅ | PostgreSQL superuser password |
REDIS_PASSWORD |
✅ | Redis authentication password |
| Variable | Required | Description |
|---|---|---|
SECRET_KEY_BASE |
✅ | Rails secret key (openssl rand -hex 64) |
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY |
✅ | Encryption key (openssl rand -hex 32) |
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT |
✅ | Encryption salt (openssl rand -hex 32) |
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY |
✅ | Encryption primary key (openssl rand -hex 32) |
POSTGRES_HOST |
✅ | Must be postgres (Docker Compose service name) |
POSTGRES_DATABASE |
✅ | Must be chatwoot_production |
POSTGRES_USERNAME |
✅ | Same as POSTGRES_USER in .env |
POSTGRES_PASSWORD |
✅ | Same as POSTGRES_PASSWORD in .env |
REDIS_URL |
✅ | redis://:<password>@redis:6379/0 |
SMTP_ADDRESS |
✅ | SMTP server hostname |
SMTP_PORT |
✅ | SMTP port (usually 587) |
SMTP_USERNAME |
✅ | SMTP username |
SMTP_PASSWORD |
✅ | SMTP password |
MAILER_SENDER_EMAIL |
✅ | Sender address for outbound e-mails |
| Variable | Required | Description |
|---|---|---|
AUTHENTICATION_API_KEY |
✅ | Master API key for Evolution API (openssl rand -hex 32) |
CHATWOOT_ENABLED |
✅ | Must be true to enable Chatwoot integration |
DEL_INSTANCE |
✅ | Set to false to keep instances alive across restarts |