Customer conversations often scatter across email threads, social media direct messages, and website live chat widgets. For growing teams, this fragmentation leads to missed handoffs, duplicate replies, and no visibility into response times. Chatwoot solves this by unifying every channel into a single self-hosted helpdesk. You get shared inboxes, canned responses, automation rules, real-time reporting, and SLA tracking without paying per-seat SaaS fees or sending conversation data to third-party clouds.
This guide deploys Chatwoot on Ubuntu with Docker Compose, Caddy for automatic HTTPS, PostgreSQL for persistent data, and Redis for caching and background job queues. By the end, you will have a production-ready stack that can serve multiple team inboxes, webhooks, chatbot integrations, and multi-language support on infrastructure you fully control.
Architecture and flow overview
Caddy sits at the edge and terminates TLS. It reverse-proxies HTTPS traffic to the Chatwoot Rails web container on port 3000. A second Chatwoot container runs Sidekiq to process background jobs such as email notifications, contact imports, and message delivery retries. PostgreSQL stores accounts, users, contacts, conversations, messages, and audit logs. Redis handles Rails caching, Action Cable pub/sub for real-time agent dashboards, and Sidekiq job queues. Persistent Docker volumes keep uploaded files and database state across container restarts and image updates. Caddy automatically provisions certificates from Let's Encrypt and renews them without manual intervention.
All services communicate over an isolated Docker bridge network. Nothing except Caddy exposes ports to the host. This means the database and Redis are unreachable from the public internet even if UFW rules are misconfigured, because they bind only inside the Docker network namespace.
Prerequisites
- Ubuntu 22.04 or 24.04 LTS server with at least 2 vCPU, 4 GB RAM, and 30 GB SSD. For teams handling more than ten thousand conversations per month, scale to 4 GB RAM and provision a separate volume for file uploads.
- Docker Engine 24.x and Docker Compose plugin installed. Verify with
docker compose version. - A DNS A record pointing
chatwoot.example.comto your server public IP. Do not start Caddy before DNS propagates or certificate issuance will fail with rate-limit penalties. - SMTP credentials for transactional email. Chatwoot sends welcome emails, password resets, conversation assignment alerts, and SLA breach warnings. Without working SMTP, agents will not receive notifications.
- UFW enabled with default-deny incoming, plus SSH allowed from your management IP range.
Step-by-step deployment
1. Create directory structure and non-root user
sudo mkdir -p /opt/chatwoot/{data,postgres-data,redis-data,caddy-config,caddy-data}
sudo useradd -r -s /usr/sbin/nologin -d /opt/chatwoot chatwoot || true
sudo chown -R 1000:1000 /opt/chatwoot/data
sudo chmod 750 /opt/chatwoot
sudo chmod 700 /opt/chatwoot/postgres-data
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw reload
The directory layout separates application uploads, database files, Redis persistence, and Caddy state. Setting ownership to UID 1000 aligns with the Chatwoot image default user and prevents permission errors when Rails writes to /app/storage.
2. Write the environment file
Chatwoot is configured entirely through environment variables. Store secrets in /opt/chatwoot/.env with restricted permissions so only root can read them.
sudo tee /opt/chatwoot/.env > /dev/null << 'EOF'
# Core
SECRET_KEY_BASE=$(openssl rand -hex 64)
FRONTEND_URL=https://chatwoot.example.com
DEFAULT_LOCALE=en
# Database
POSTGRES_HOST=postgres
POSTGRES_USERNAME=chatwoot
POSTGRES_PASSWORD=$(openssl rand -hex 32)
POSTGRES_DATABASE=chatwoot_production
RAILS_ENV=production
RAILS_LOG_TO_STDOUT=true
# Redis
REDIS_URL=redis://redis:6379/0
REDIS_PASSWORD=$(openssl rand -hex 32)
# Action Cable / websockets
ACTION_CABLE_URL=wss://chatwoot.example.com/cable
ACTION_CABLE_ALLOWED_REQUEST_ORIGINS=https://chatwoot.example.com
# SMTP (replace with your provider)
SMTP_ADDRESS=smtp.sendgrid.net
SMTP_PORT=587
SMTP_USERNAME=apikey
SMTP_PASSWORD=your_sendgrid_api_key
SMTP_AUTHENTICATION=plain
SMTP_ENABLE_STARTTLS_AUTO=true
[email protected]
# Storage
ACTIVE_STORAGE_SERVICE=local
RAILS_SERVE_STATIC_FILES=true
EOF
sudo chmod 600 /opt/chatwoot/.env
sudo chown root:root /opt/chatwoot/.env
Replace chatwoot.example.com and SMTP values with your real domain and provider credentials. If you use Amazon SES, set SMTP_ADDRESS=email-smtp.us-east-1.amazonaws.com and SMTP_USERNAME to your SMTP username. For Microsoft 365, use smtp.office365.com with port 587 and modern authentication. You can verify SMTP connectivity later from inside the container with swaks or Ruby Net::SMTP before going live.
3. Write Docker Compose
sudo tee /opt/chatwoot/docker-compose.yml > /dev/null << 'EOF'
version: "3.8"
services:
postgres:
image: postgres:16-alpine
container_name: chatwoot-postgres
restart: unless-stopped
env_file: .env
environment:
POSTGRES_USER: ${POSTGRES_USERNAME}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DATABASE}
volumes:
- /opt/chatwoot/postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USERNAME} -d ${POSTGRES_DATABASE}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- chatwoot
redis:
image: redis:7-alpine
container_name: chatwoot-redis
restart: unless-stopped
command: >
sh -c "redis-server --requirepass $${REDIS_PASSWORD}"
env_file: .env
volumes:
- /opt/chatwoot/redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- chatwoot
chatwoot:
image: chatwoot/chatwoot:latest
container_name: chatwoot-web
restart: unless-stopped
env_file: .env
command: ["bundle", "exec", "rails", "s", "-p", "3000", "-b", "0.0.0.0"]
volumes:
- /opt/chatwoot/data:/app/storage
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- chatwoot
sidekiq:
image: chatwoot/chatwoot:latest
container_name: chatwoot-worker
restart: unless-stopped
env_file: .env
command: ["bundle", "exec", "sidekiq", "-C", "config/sidekiq.yml"]
volumes:
- /opt/chatwoot/data:/app/storage
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- chatwoot
caddy:
image: caddy:2-alpine
container_name: chatwoot-caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- /opt/chatwoot/caddy-config:/etc/caddy
- /opt/chatwoot/caddy-data:/data
depends_on:
- chatwoot
networks:
- chatwoot
networks:
chatwoot:
driver: bridge
EOF
The Compose file defines healthchecks so the web and worker containers start only after PostgreSQL and Redis are ready. This eliminates race conditions that cause Rails to crash on first boot because the database is still initializing.
4. Write the Caddyfile
sudo tee /opt/chatwoot/caddy-config/Caddyfile > /dev/null << 'EOF'
chatwoot.example.com {
reverse_proxy chatwoot:3000
encode gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
}
}
EOF
Replace chatwoot.example.com with your domain. The security headers harden the installation against clickjacking and MIME-sniffing attacks. Gzip compression reduces payload size for the JavaScript-heavy dashboard.
5. Bootstrap the database
cd /opt/chatwoot
sudo docker compose up -d postgres redis
sleep 15
sudo docker compose run --rm chatwoot bundle exec rails db:chatwoot_prepare
The db:chatwoot_prepare task creates the database, runs migrations, and seeds the default account. If you see connection errors, increase the sleep duration or inspect PostgreSQL logs with docker compose logs postgres.
6. Start all services
cd /opt/chatwoot
sudo docker compose up -d
Caddy will request a TLS certificate within seconds. You can follow progress with docker compose logs -f caddy.
Configuration and secrets handling best practices
Rotate SECRET_KEY_BASE and POSTGRES_PASSWORD every 90 days. Back up /opt/chatwoot/.env to an encrypted store such as HashiCorp Vault, 1Password, or Bitwarden. Never commit the file to Git. If you run multiple Chatwoot instances for different brands or subsidiaries, use separate .env files and Docker Compose projects to isolate databases and Redis namespaces.
For object storage instead of local disk, set ACTIVE_STORAGE_SERVICE=s3 and add S3_BUCKET_NAME, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_REGION. This offloads file uploads to S3-compatible storage, simplifies multi-node deployments, and protects attachments during server failures. If you use MinIO or another S3-compatible provider, also set S3_ENDPOINT and S3_FORCE_PATH_STYLE=true.
Enable two-factor authentication for the first admin account immediately after registration. Create a dedicated super-admin role and avoid using the initial account for day-to-day replies to reduce blast radius if credentials are phished.
Verification checklist
- Open
https://chatwoot.example.comand register the first admin account - Create an inbox (Website or Email) and send a test message from the visitor side
- Assign the conversation to an agent and confirm the assignment email arrives
- Check Sidekiq health in
/monitoringafter logging in as admin - Verify SMTP by triggering a password reset email from the login screen
- Confirm Caddy auto-HTTPS:
curl -I https://chatwoot.example.comshould return HTTP 200 with a valid certificate - Check Docker logs for errors:
sudo docker compose logs -f chatwoot - Test websockets by opening the agent dashboard in two browsers and watching messages appear in real time
Common issues and fixes
Database connection errors on first start: If Chatwoot exits before postgres is ready, run sudo docker compose down and then sudo docker compose up -d again. The healthcheck conditions normally prevent this, but slow disks can delay PostgreSQL startup beyond the retry window.
Assets 404 or styles missing: Ensure RAILS_SERVE_STATIC_FILES=true is set. If you build a custom image from source, run RAILS_ENV=production bundle exec rails assets:precompile inside the Dockerfile before starting the container.
Websockets not connecting: Verify ACTION_CABLE_URL uses wss:// and Caddy is proxying with default timeout settings. Check browser DevTools Network tab for 403s on /cable. If you see 404, the Action Cable mount path may be missing from your reverse proxy configuration.
Emails stuck in pending: Confirm SMTP credentials and restart Sidekiq: sudo docker compose restart sidekiq. You can inspect the Sidekiq retries queue at /monitoring. Common root causes are incorrect SMTP_AUTHENTICATION values or IP-based sending limits at the provider.
Disk growth from uploads: Schedule a nightly cron to clean orphaned Active Storage blobs, or move to S3 and set a lifecycle policy. On local storage, monitor /opt/chatwoot/data with ncdu or Prometheus Node Exporter.
High memory usage: Sidekiq workers can accumulate memory over time. Set a memory limit on the Sidekiq container using Docker Compose deploy.resources.limits.memory and configure a systemd watchdog or Docker restart policy to recycle the process before OOM kills occur.
FAQ
Can I run Chatwoot without Redis?
No. Redis is required for Sidekiq job queues, Rails caching, and Action Cable real-time messaging. You can host Redis on a separate instance or use a managed Redis service by changing REDIS_URL to point to the external endpoint.
How do I update to a newer Chatwoot release?
Pull the latest image, run migrations, and restart:
cd /opt/chatwoot
sudo docker compose pull
sudo docker compose run --rm chatwoot bundle exec rails db:migrate
sudo docker compose up -d
Always review the upstream release notes before upgrading. Some releases introduce new required environment variables or breaking changes to the database schema.
Does Chatwoot support SAML or OIDC SSO?
Yes. Enterprise edition supports SAML, OIDC, and OAuth providers. Set ENABLE_ACCOUNT_SIGNUP=false and configure SSO_AUTH_TOKEN_SIGNING_KEY plus provider-specific variables such as OIDC_ISSUER, OIDC_CLIENT_ID, and OIDC_CLIENT_SECRET. Community edition supports basic OAuth with Google, Facebook, and Twitter out of the box.
How do I back up the database?
Use pg_dump inside the postgres container:
sudo docker exec chatwoot-postgres pg_dump -U chatwoot chatwoot_production > chatwoot_$(date +%F).sql
Store the dump off-site on S3 or a backup server. For point-in-time recovery, enable WAL archiving in PostgreSQL and use a tool like pgBackRest or Barman.
Can I use an external PostgreSQL or Redis managed service?
Yes. Update POSTGRES_HOST and REDIS_URL to point to your external endpoint. Remove the internal postgres and redis services from Docker Compose if they are no longer needed. Managed services reduce operational overhead but add network latency; keep the database in the same region as your application server.
What ports need to be open for agents and visitors?
Only 443/tcp is required for end users. Port 80/tcp should remain open for Caddy HTTP-to-HTTPS redirects. Do not expose 3000 or 5432 to the internet. If you use a cloud security group, restrict 22/tcp to your office or bastion IP.
How do I monitor Sidekiq queue depth?
Log in as an admin and visit /monitoring. You can also scrape Sidekiq metrics by exposing the Prometheus exporter and pointing Grafana to it. Set alerts on queue depth and retry set size so you catch email delivery failures early.
Is there an official mobile app for Chatwoot?
Yes. Chatwoot provides mobile apps for iOS and Android. Self-hosted installations can connect by entering the custom server URL during onboarding. Ensure your FRONTEND_URL is publicly reachable and your SSL certificate is valid for mobile browsers to avoid mixed-content warnings.
Internal links
- Production Guide: Deploy Zammad with Docker Compose + Caddy + PostgreSQL + Elasticsearch
- Production Guide: Deploy n8n with Docker Compose + Caddy + PostgreSQL + Redis on Ubuntu
- Production Guide: Deploy Matomo with Docker Compose + Caddy + MariaDB on Ubuntu
Talk to us
If you want this implemented with hardened defaults, observability, and tested recovery playbooks, our team can help.