Chatwoot is an open-source customer support platform that brings website live chat, email, WhatsApp, Telegram and other channels into a single shared inbox for your team. In this tutorial you will deploy Chatwoot on Ubuntu 24.04 using the official production Docker Compose file (Rails app, Sidekiq worker, PostgreSQL and Redis), publish it through Nginx with a Let's Encrypt certificate, configure outgoing email and add a live chat widget to a website.

Prerequisites

To follow this tutorial you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with at least 4 GB of RAM (Rails, Sidekiq, PostgreSQL and Redis together use around 2 GB at idle) and 20 GB of free disk space.
  • A non-root user with sudo privileges.
  • Docker Engine and the Docker Compose plugin installed from Docker's official repository. docker compose version must print a v2 version.
  • A domain or subdomain (this guide uses chat.your_domain) with a DNS A record pointing to your_server_ip.
  • SMTP credentials for sending email (invitations, password resets and conversation notifications).

Step 1 - Downloading the Chatwoot configuration

Create a directory for the deployment:

sudo mkdir -p /opt/chatwoot
sudo chown $USER:$USER /opt/chatwoot
cd /opt/chatwoot

Chatwoot maintains an example environment file and a production Compose file in its repository. Download both:

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

The Compose file defines four services: rails (web app), sidekiq (background jobs), postgres and redis. The Rails port is published only on 127.0.0.1:3000, so the app is reachable from the Internet only through Nginx. Confirm it:

grep "3000:3000" docker-compose.yaml
      - '127.0.0.1:3000:3000'

Step 2 - Setting secrets and the public URL

Generate the Rails secret, the PostgreSQL password and the Redis password, and write them into .env:

sed -i "s|^SECRET_KEY_BASE=.*|SECRET_KEY_BASE=$(openssl rand -hex 64)|" .env
sed -i "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=$(openssl rand -hex 24)|" .env
sed -i "s|^REDIS_PASSWORD=.*|REDIS_PASSWORD=$(openssl rand -hex 24)|" .env

Now open the file to set the public URL, disable open sign-ups and configure email:

nano /opt/chatwoot/.env

Find and adjust these variables, replacing the placeholders with your values:

FRONTEND_URL=https://chat.your_domain
ENABLE_ACCOUNT_SIGNUP=false

MAILER_SENDER_EMAIL=Support <support@your_domain>
SMTP_DOMAIN=your_domain
SMTP_ADDRESS=smtp.your_provider.com
SMTP_PORT=587
SMTP_USERNAME=your_smtp_user
SMTP_PASSWORD=your_smtp_password
SMTP_AUTHENTICATION=login
SMTP_ENABLE_STARTTLS_AUTO=true
  • FRONTEND_URL is used to build links in emails and the widget script, so it must be the exact HTTPS URL.
  • ENABLE_ACCOUNT_SIGNUP=false prevents strangers from creating accounts on your instance. You will create the first account through the installation onboarding.

Leave POSTGRES_HOST=postgres and REDIS_URL=redis://redis:6379 as they are: they point to the service names in the Compose file. Check the values you set:

grep -E '^(FRONTEND_URL|ENABLE_ACCOUNT_SIGNUP|POSTGRES_HOST|POSTGRES_DATABASE|REDIS_URL|SMTP_ADDRESS)=' .env
FRONTEND_URL=https://chat.your_domain
ENABLE_ACCOUNT_SIGNUP=false
POSTGRES_HOST=postgres
POSTGRES_DATABASE=chatwoot_production
REDIS_URL=redis://redis:6379
SMTP_ADDRESS=smtp.your_provider.com

The postgres container creates its superuser from the POSTGRES_PASSWORD set in the Compose file, not from .env. Print the generated password and copy it:

grep '^POSTGRES_PASSWORD=' .env

Open docker-compose.yaml, find the environment list of the postgres service and set the same value on its POSTGRES_PASSWORD line, leaving the other lines as they are:

nano /opt/chatwoot/docker-compose.yaml
      - POSTGRES_PASSWORD=paste_the_generated_password_here

Validate the file:

docker compose config --quiet && echo "docker-compose.yaml OK"

Step 3 - Preparing the database and starting Chatwoot

Run the database preparation task once. It starts PostgreSQL and Redis, creates the database and runs the migrations:

docker compose run --rm rails bundle exec rails db:chatwoot_prepare

The first run pulls the images and takes a few minutes. When it finishes without errors, start all services:

docker compose up -d
docker compose ps
NAME                  SERVICE    STATUS          PORTS
chatwoot-postgres-1   postgres   Up 2 minutes    127.0.0.1:5432->5432/tcp
chatwoot-rails-1      rails      Up 40 seconds   127.0.0.1:3000->3000/tcp
chatwoot-redis-1      redis      Up 2 minutes    127.0.0.1:6379->6379/tcp
chatwoot-sidekiq-1    sidekiq    Up 40 seconds

Rails needs about 30 seconds to boot. Then check that the API answers:

curl -sI http://127.0.0.1:3000/api | head -n 1
HTTP/1.1 200 OK

If it does not, read the logs with docker compose logs --tail=50 rails.

Step 4 - Configuring Nginx and HTTPS

Install Nginx and Certbot and open the web ports:

sudo apt install nginx certbot python3-certbot-nginx
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

Chatwoot uses Action Cable (WebSockets on /cable) to push new messages to agents and to the chat widget, so the proxy must forward the Upgrade headers. Its API also uses an api_access_token header, which contains underscores; Nginx drops such headers unless underscores_in_headers is enabled. Create the server block:

sudo nano /etc/nginx/sites-available/chatwoot
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    listen [::]:80;
    server_name chat.your_domain;

    underscores_in_headers on;
    client_max_body_size 40M;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 3600s;
    }
}

client_max_body_size allows agents and customers to upload attachments. Enable the site and reload Nginx:

sudo ln -s /etc/nginx/sites-available/chatwoot /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Request the certificate. Certbot adds the TLS configuration and an HTTP to HTTPS redirect:

sudo certbot --nginx -d chat.your_domain

Verify the public endpoint:

curl -sI https://chat.your_domain/api | head -n 1
HTTP/2 200

The certbot.timer systemd timer renews the certificate automatically; sudo certbot renew --dry-run confirms it.

Step 5 - Creating the administrator account

Open https://chat.your_domain. On a fresh installation Chatwoot shows an onboarding form. Enter your name, company name, email and a strong password. This creates the first account and a super admin user.

After logging in you land on the conversations dashboard. The super admin console, where you manage accounts, users and instance-wide settings, is at https://chat.your_domain/super_admin.

Step 6 - Adding a website live chat inbox

An inbox is a channel through which customers reach you. To add live chat to a website:

  1. Go to Settings > Inboxes and click Add Inbox.
  2. Choose Website, enter the website name and domain, and customize the widget color and welcome message.
  3. Select the agents who will answer this inbox and click Add agents.
  4. Chatwoot shows a JavaScript snippet. Paste it before the closing </body> tag of your website.

Reload your website: a chat bubble appears in the corner. Send a test message from the widget and it shows up in Conversations in the Chatwoot dashboard. Reply from the dashboard and the answer appears in the widget in real time, which confirms that WebSockets work through Nginx.

Step 7 - Inviting agents and organizing work

  • Agents: go to Settings > Agents, click Add Agent, enter a name and email and choose the Agent or Administrator role. The agent receives an invitation email, which also confirms that SMTP works.
  • Teams: under Settings > Teams, group agents (for example Billing and Technical) so conversations can be assigned to a team.
  • Labels and automation: create labels under Settings > Labels, and use Settings > Automation to assign or label conversations based on conditions such as the inbox or message content.

If the invitation email does not arrive, check docker compose logs --tail=100 sidekiq: emails are sent by the Sidekiq worker, and SMTP errors are logged there.

Step 8 - Backing up Chatwoot

Conversations, contacts and settings live in PostgreSQL; attachments live in the storage_data volume (with the default ACTIVE_STORAGE_SERVICE=local). Create a backup directory and dump the database, using the database name from POSTGRES_DATABASE in .env:

sudo install -d -m 700 -o $USER /var/backups/chatwoot
cd /opt/chatwoot
docker compose exec -T postgres pg_dump -U postgres chatwoot_production | gzip > /var/backups/chatwoot/chatwoot-db-$(date +%F).sql.gz

Find the full name of the storage volume, which Compose prefixes with the project name:

docker volume ls --filter name=storage
DRIVER    VOLUME NAME
local     chatwoot_storage_data

Archive it:

docker run --rm -v chatwoot_storage_data:/data:ro -v /var/backups/chatwoot:/backup alpine \
  tar czf /backup/chatwoot-storage-$(date +%F).tar.gz -C /data .
ls -lh /var/backups/chatwoot

Keep a copy of /opt/chatwoot/.env and docker-compose.yaml together with these files, and copy everything off the server.

Step 9 - Updating Chatwoot

Take a backup first, then pull the new images, run the migrations and recreate the containers:

cd /opt/chatwoot
docker compose pull
docker compose run --rm rails bundle exec rails db:chatwoot_prepare
docker compose up -d
docker image prune -f

Check the release notes before major upgrades, since new versions sometimes add required variables to .env.example.

Troubleshooting

  • Messages only appear after reloading the page: WebSockets to /cable are not getting through. Check the map block and the Upgrade/Connection headers in Nginx.
  • API calls with api_access_token return 401 through Nginx but work on port 3000: underscores_in_headers on; is missing from the server block.
  • db:chatwoot_prepare fails with a password authentication error: the POSTGRES_PASSWORD in .env does not match the one in docker-compose.yaml. If the database volume was already initialized with another password, remove it with docker compose down -v (this deletes all data, so only do it on a fresh install) and run the task again.
  • Links in emails point to the wrong host: FRONTEND_URL is wrong. Fix it and run docker compose up -d.

Conclusion

Chatwoot is now running on Ubuntu 24.04 behind Nginx with HTTPS, with working email, a website live chat inbox and a backup and update routine. As next steps, connect an email inbox so customer emails become conversations, add canned responses for common questions, and schedule the backup commands with a systemd timer or cron.