bolt Valebyte VPS from $4/mo — NVMe, 60s deploy.

Get a VPS arrow_forward
eco Beginner Tutorial/How-to

Installing Chatwoot on a VPS: Docker, PostgreSQL, Redis, SSL, and Email Configuration

calendar_month Oct 08, 2026 schedule 18 min read visibility 27 views
Установка Chatwoot на VPS: Docker, PostgreSQL, Redis, SSL и настройка email
info

Need a server for this guide? We offer dedicated servers and VPS in 50+ countries with instant setup.

Need a server for this guide?

Deploy a VPS or dedicated server in minutes.

Installing Chatwoot on a VPS: Docker, PostgreSQL, Redis, SSL, and email configuration

TL;DR

Chatwoot is a self-hosted platform for customer support through a website widget, email, Telegram, WhatsApp, and other channels. In this guide, you will deploy Chatwoot on an Ubuntu VPS using Docker, connect PostgreSQL and Redis, configure HTTPS through Caddy, SMTP for outgoing email, backups, and secure maintenance.

  • For a small team, a VPS with 2 vCPU, 4 GB RAM, and 50–80 GB NVMe is sufficient.
  • Chatwoot runs in containers: the web application, background worker, PostgreSQL, Redis, and Caddy.
  • The Let’s Encrypt HTTPS certificate is issued and renewed automatically through Caddy.
  • Outgoing email is configured through SMTP variables in the .env file.
  • Before starting, you need to create the database using the db:chatwoot_prepare command.
  • Critical data to back up: PostgreSQL, .env, Caddy data, and user uploads.

What we are configuring and why

Chatwoot is an open-source customer support system and omnichannel inbox. It consolidates customer inquiries into one interface: messages from web chat, email, Telegram, Facebook Messenger, Instagram, WhatsApp Business API, and other integrations. Operators can assign conversations to each other, use labels, response templates, automation, SLAs, and a knowledge base.

This guide installs a self-hosted Chatwoot instance on your own VPS. It will be available under your domain name, for example chat.example.com, through a secure HTTPS connection. Conversation data, contacts, attachments, and settings will be stored on your server rather than in a third-party SaaS account.

What will work after installation

  • the Chatwoot web interface for administrators and operators;
  • a live chat widget that can be added to your website;
  • PostgreSQL 16 as the primary database;
  • Redis 7 for queues, cache, and background tasks;
  • a Sidekiq worker for sending email, processing webhooks, and background operations;
  • Caddy 2 as a reverse proxy and TLS certificate manager;
  • SMTP delivery for notifications, user invitations, and replies from the email channel;
  • automatic PostgreSQL backups to S3-compatible storage.

Cloud-managed or self-hosted

Criterion Cloud service Self-hosted Chatwoot on a VPS
Startup speed A few minutes; the infrastructure is already ready You need to configure the server, DNS, SSL, and backups
Data control Data is stored with the service provider You control the data, logs, and backups
Customization Limited by the plan and interface You can modify configurations, integrations, and the application version
Operations Updates and backups are usually handled by the service Updates, security, and recovery are your responsibility
Cost as the team grows Often depends on the number of agents and channels Depends mainly on server and storage resources

The self-hosted option is suitable for teams that need control over personal data, integration with internal systems, predictable infrastructure costs, or hosting in a required jurisdiction. It is also convenient for SaaS projects, agencies, and internal support departments.

Before starting, prepare a domain or subdomain, for example chat.example.com. Its A record must point to the public IPv4 address of the VPS. Ports 80 and 443 must be accessible from the internet for automatic certificate issuance.

What VPS configuration is needed for this task

Chatwoot load depends not only on the number of operators, but also on the number of simultaneous web chat visitors, attachment sizes, connected channels, and automation activity. PostgreSQL and Sidekiq are sensitive to insufficient memory: a server with 1 GB RAM should not be used for a production deployment.

Scenario vCPU RAM NVMe disk Suitable for
Test environment 2 2 GB 30 GB Testing, one administrator, without a large number of attachments
Minimum production 2 4 GB 50–80 GB Up to 5–10 agents and a moderate inquiry volume
Working team 4 8 GB 100–160 GB 10–30 agents, active channels, and attachments
High load 8+ 16+ GB 250+ GB Many conversations, integrations, APIs, and long-term media storage

A practical starting configuration is 2 vCPU, 4 GB RAM, 80 GB NVMe, and a connection from 100 Mbps. This is sufficient for PostgreSQL, Redis, Caddy, the web container, and the worker to run simultaneously. You can choose a VPS with the specified characteristics or a similar server from another provider.

Why disk space reserve matters

Docker images are not the only disk consumers. Space is used by the PostgreSQL database, temporary files, container logs, conversation attachments, backups before they are sent to external storage, and Caddy data. Do not size your disk too tightly: keep at least 25–30% free space. If the partition fills up, PostgreSQL may stop unexpectedly or disrupt transaction processing.

When you need dedicated rather than a VPS

A dedicated server makes sense under consistently high load, when resource isolation is required, when storing a large number of files, or when Chatwoot runs alongside other resource-intensive services. For example, consider a dedicated server with 50+ active agents, tens of thousands of conversations per month, local storage of large media files, and intensive analytics.

For typical small business support, a VPS is usually better: it is easier to scale CPU, RAM, and disk, costs less initially, and does not require reserving excess resources. Do not host production Chatwoot on the same server as a public database, test CI jobs, or unverified containers.

How to choose a location

Location affects web chat latency, email routing, and personal data processing requirements. Choose a region closer to your operators and primary audience. If you store conversations of customers from the EU, check internal legal requirements for the hosting region and data processing agreements in advance.

Server preparation

Ubuntu Server 24.04 LTS is used below. As of 2026, it is a suitable LTS base for Docker deployments: it includes a current kernel, long-term support, and a stable package set. Connect to the server as the root user provided by your hosting provider.

Update the operating system

The command installs the latest security updates, after which the server is rebooted if the kernel was updated.

apt update && apt upgrade -y
apt autoremove -y
reboot

After rebooting, reconnect through SSH and create a separate user for administration. Do not work permanently as root: this increases the risk of accidentally deleting data or running a dangerous command without restrictions.

adduser deploy
usermod -aG sudo deploy
mkdir -p /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chown -R deploy:deploy /home/deploy/.ssh

Copy your public SSH key to the authorization file. Run the command on your local computer, replacing the server IP address.

ssh-copy-id deploy@SERVER_IP

Test login in a second terminal. Do not close the current root session until you verify that the key works.

ssh deploy@SERVER_IP
sudo whoami

The expected output of the second command is root. After verification, disable root login and password authentication. First, save a backup copy of the SSH configuration.

sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
sudo sed -i 's/^#\?PermitRootLogin./PermitRootLogin no/' /etc/ssh/sshd_config
sudo sed -i 's/^#\?PasswordAuthentication./PasswordAuthentication no/' /etc/ssh/sshd_config
sudo sshd -t && sudo systemctl reload ssh

Install basic utilities and brute-force protection

The packages below are needed for diagnostics, downloading files, working with repositories, and securing SSH. Even with password authentication disabled, Fail2ban is useful as an additional control layer.

sudo apt install -y ca-certificates curl gnupg git jq \
  ufw fail2ban unattended-upgrades apt-transport-https \
  software-properties-common

Configure the firewall. Only SSH, HTTP, and HTTPS are opened. PostgreSQL on port 5432 and Redis on port 6379 must not be exposed externally: containers will communicate only within the internal Docker network.

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose

Create a minimal local Fail2ban configuration for SSH. The values mean: ban an IP for one hour if it makes five failed attempts within ten minutes.

sudo tee /etc/fail2ban/jail.d/sshd.local <<'EOF'
[sshd]
enabled = true
maxretry = 5
findtime = 10m
bantime = 1h
EOF

sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshd

Create a working directory

All stack files will be located in /opt/chatwoot. This path is convenient to back up, migrate, and inspect. Do not store secrets in the home directory if there are multiple administrators on the server.

sudo mkdir -p /opt/chatwoot
sudo chown -R deploy:deploy /opt/chatwoot
cd /opt/chatwoot

Software Installation — Step by Step

Docker Engine 27+ and Docker Compose Plugin v2 are used for the installation. In production, do not install PostgreSQL and Redis directly via apt if you plan to run them in Docker: mixing the two approaches complicates network diagnostics, updates, and backups.

Install Docker Engine from the official repository

First, add the Docker key and repository for Ubuntu.

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
  sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

The following command adds the repository matching your Ubuntu architecture and version.

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update

Install Docker Engine, the Compose Plugin, and the required container networking components.

sudo apt install -y docker-ce docker-ce-cli containerd.io \
  docker-buildx-plugin docker-compose-plugin

sudo systemctl enable --now docker
sudo usermod -aG docker deploy

Log out of the SSH session and reconnect for the docker group to take effect. Then check the versions.

exit
ssh deploy@SERVER_IP
docker --version
docker compose version
docker run --rm hello-world

As of 2026, use Docker Engine 27 or newer and Docker Compose v2. If the docker run command returns a welcome message, the daemon is working correctly.

Create the persistent data structure

Docker named volumes survive container recreation, but it is more convenient to explicitly create directories for Caddy and local attachments. The storage directory will be used by Chatwoot for local file storage.

cd /opt/chatwoot
mkdir -p caddy/data caddy/config storage backups
chmod 700 backups
find /opt/chatwoot -maxdepth 2 -type d -print

Generate secrets

Chatwoot uses SECRET_KEY_BASE to sign sessions and protect application cryptographic data. Losing this key may log users out, while exposing it creates a risk of session compromise. Generate two random values and do not send them through messengers or commit them to repositories.

openssl rand -hex 64
openssl rand -hex 32

The first value will be used as SECRET_KEY_BASE, and the second as the PostgreSQL password. In the next section, they will be placed in the .env file, whose permissions will be restricted.

Check DNS before starting

Before starting Caddy, the domain must already resolve to the server's IP address. Replace the name with your own. If the output shows a different address or is empty, correct the A record with your DNS provider and wait for the zone to update.

getent ahostsv4 chat.example.com
curl -4 ifconfig.me
echo

Chatwoot, SSL, and Email Configuration

In this setup, all services are defined in a single compose.yaml file. PostgreSQL and Redis do not expose ports on the host, so they cannot be accessed directly from the internet. Caddy is the only container that accepts external requests on ports 80 and 443.

Create the environment variables file

Create /opt/chatwoot/.env. Replace the domain, administrator email, passwords, and SMTP settings. The FRONTEND_URL value must start with https:// and must not have a trailing slash.

cd /opt/chatwoot
nano .env
POSTGRES_DB=chatwoot_production
POSTGRES_USER=chatwoot
POSTGRES_PASSWORD=CHANGE_TO_A_LONG_RANDOM_DATABASE_PASSWORD
POSTGRES_HOST=postgres
POSTGRES_PORT=5432

REDIS_URL=redis://redis:6379
RAILS_ENV=production
NODE_ENV=production
INSTALLATION_ENV=docker
SECRET_KEY_BASE=CHANGE_TO_128_HEX_CHARACTERS_FROM_OPENSSL

FRONTEND_URL=https://chat.example.com
DEFAULT_LOCALE=ru
RAILS_LOG_TO_STDOUT=true
RAILS_SERVE_STATIC_FILES=true
ENABLE_ACCOUNT_SIGNUP=false

ACTIVE_STORAGE_SERVICE=local
LOCAL_STORAGE_PATH=/app/storage

[email protected]
SMTP_ADDRESS=smtp.example.com
SMTP_PORT=587
SMTP_USERNAME=smtp-login
SMTP_PASSWORD=CHANGE_TO_SMTP_PASSWORD
SMTP_DOMAIN=example.com
SMTP_AUTHENTICATION=plain
SMTP_ENABLE_STARTTLS_AUTO=true
SMTP_OPENSSL_VERIFY_MODE=peer

[email protected]

For SMTP, use a dedicated mailbox or SMTP account. Email services often require an app password rather than the mailbox's primary password. Port 587 is used with STARTTLS, while port 465 uses TLS immediately after connecting; 465 usually requires additional adapter configuration, so choose 587 for the initial deployment.

Restrict access to the file: only root and the deploy user should be able to read it.

chmod 600 /opt/chatwoot/.env
ls -l /opt/chatwoot/.env

Create the Docker Compose configuration

The stable Chatwoot image from the 4.x branch is used below. Before a major update, pin a specific tag, such as v4.x.y, after reviewing the release notes. The latest tag is convenient for testing, but makes production updates less predictable.

nano compose.yaml
services:
  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    env_file: .env
    environment:
      POSTGRES_DB: ${POSTGRES_DB}
      POSTGRES_USER: ${POSTGRES_USER}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
      interval: 10s
      timeout: 5s
      retries: 10
    networks:
      - chatwoot_internal

  redis:
    image: redis:7.4-alpine
    restart: unless-stopped
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 10
    networks:
      - chatwoot_internal

  rails:
    image: chatwoot/chatwoot:v4.0.2
    restart: unless-stopped
    env_file: .env
    command: bundle exec rails server -p 3000 -b 0.0.0.0
    entrypoint: docker/entrypoints/docker-entrypoint.sh
    volumes:
      - ./storage:/app/storage
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - chatwoot_internal

  worker:
    image: chatwoot/chatwoot:v4.0.2
    restart: unless-stopped
    env_file: .env
    command: bundle exec sidekiq -C config/sidekiq.yml
    entrypoint: docker/entrypoints/docker-entrypoint.sh
    volumes:
      - ./storage:/app/storage
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - chatwoot_internal

  caddy:
    image: caddy:2.10-alpine
    restart: unless-stopped
    env_file: .env
    ports:
      - "80:80"
      - "443:443"
      - "443:443/udp"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./caddy/data:/data
      - ./caddy/config:/config
    depends_on:
      - rails
    networks:
      - chatwoot_internal

volumes:
  postgres_data:
  redis_data:

networks:
  chatwoot_internal:
    driver: bridge

If the current stable Chatwoot release is newer than v4.0.2 at the time of installation, replace the tags for the rails and worker containers with the same tested tag from the current stable branch. Never update only one of these two containers: the web process and Sidekiq must run the same application version.

Configure Caddy and TLS

Caddy will request a Let’s Encrypt certificate automatically, configure the HTTP-to-HTTPS redirect, and renew the certificate. Certificates and the ACME account are stored in ./caddy/data, so they will not disappear after the container is recreated.

nano Caddyfile
CADDY_EMAIL {
  email {$CADDY_EMAIL}
}

chat.example.com {
  encode zstd gzip

  reverse_proxy rails:3000

  header {
    -Server
    Strict-Transport-Security "max-age=31536000; includeSubDomains"
    X-Content-Type-Options "nosniff"
    X-Frame-Options "SAMEORIGIN"
    Referrer-Policy "strict-origin-when-cross-origin"
  }

  log {
    output stdout
    format json
  }
}

Replace chat.example.com in the Caddyfile with the same domain specified in FRONTEND_URL. The CADDY_EMAIL variable from the .env file is used inside Caddy through the {$CADDY_EMAIL} syntax.

Start the infrastructure containers and prepare the database

First, download the images and start PostgreSQL with Redis. Then run the migrations and initial database setup. The db:chatwoot_prepare command creates the table structure and applies migrations.

cd /opt/chatwoot
docker compose pull
docker compose up -d postgres redis
docker compose ps

Wait for PostgreSQL and Redis to reach the healthy status. Then start a one-time container with the database preparation task.

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

If the command finishes without an error, start the application, worker, and reverse proxy. The -d option starts services in the background.

docker compose up -d rails worker caddy
docker compose ps
docker compose logs --tail=100 caddy

Verify operation

Check the Caddy response from the VPS itself. A 200, 301, or 302 status code means the route is responding. On the first request, Caddy may take several seconds to obtain the certificate.

curl -I http://chat.example.com
curl -I https://chat.example.com
docker compose logs --tail=100 rails
docker compose logs --tail=100 worker

Open https://chat.example.com in a browser. On first launch, Chatwoot will prompt you to create an administrator account and organization. Since ENABLE_ACCOUNT_SIGNUP=false, public registration will be disabled after the initial account is created; add new agents through invitations in the administrator interface.

Test SMTP and configure the email channel

After creating the administrator account, go to the organization settings section in the interface and test inviting a new agent: this is a simple outgoing email test. If the message does not arrive, first review the worker logs, because email delivery is handled by a background job.

docker compose logs -f worker

To receive inquiries by email, create an Email Channel inbox in the Chatwoot panel. The service will display a forwarding address or integration settings. Configure forwarding from your support address to this address with your email provider, or use a supported mail channel according to the interface instructions.

For good outgoing email deliverability, configure SPF, DKIM, and DMARC DNS records for the sender domain. SMTP may technically work without them, but invitation and reply emails are more likely to end up in spam. The MAILER_SENDER_EMAIL address must belong to a domain for which these records are configured.

Add the web widget to your website

After creating a Website Inbox, Chatwoot will display a ready-made JavaScript snippet. Insert it before the closing </body> tag of the website. Do not copy an example with someone else's website identifier: use the code generated by your own Chatwoot instance, otherwise conversations will be associated with the wrong inbox.

Backups and Maintenance

A production server without verified recovery cannot be considered protected. For Chatwoot, you need to back up more than just PostgreSQL: some critical data is stored in the configuration and local attachment storage. Redis is usually not the primary data source, but its volume can be included in a full disaster recovery backup.

What Must Be Saved

  • PostgreSQL: accounts, contacts, conversations, messages, inbox settings, and integrations.
  • .env file: application secrets, SMTP settings, database password, public URL.
  • storage directory: attachments if ACTIVE_STORAGE_SERVICE=local is used.
  • caddy/data directory: certificates and ACME data; they can be restored again, but keeping them is useful.
  • compose.yaml and Caddyfile: infrastructure configuration.

Do not rely on a Docker volume alone as a backup. The volume resides on the same disk and will not help if the VPS is deleted, an administrator makes a mistake, the file system is corrupted, or the account is suspended. At least one copy must be sent to external S3-compatible storage, a separate backup VPS, or object storage under another account.

Install restic

Restic encrypts archives on the server before uploading them to remote storage. The example below uses an S3-compatible bucket. Create the bucket in advance and issue a separate access key with access only to this bucket.

sudo apt install -y restic
sudo mkdir -p /etc/restic
sudo chmod 700 /etc/restic

Create an environment file for restic. Obtain the endpoint, bucket, and key values from your S3 provider. Do not add this file to Git.

sudo nano /etc/restic/chatwoot.env
RESTIC_REPOSITORY=s3:https://s3.example.net/chatwoot-backups
RESTIC_PASSWORD=CHANGE_TO_A_LONG_BACKUP_ENCRYPTION_PASSWORD
AWS_ACCESS_KEY_ID=CHANGE_TO_ACCESS_KEY
AWS_SECRET_ACCESS_KEY=CHANGE_TO_SECRET_KEY
sudo chmod 600 /etc/restic/chatwoot.env
sudo chown root:root /etc/restic/chatwoot.env

Create a Backup Script

The script creates a consistent PostgreSQL dump using pg_dump, adds configuration files and file storage to an encrypted restic archive, then removes the temporary SQL file. For large installations, it is better to move PostgreSQL to a separate managed service or configure physical backups, but a logical dump is suitable for most small teams.

sudo nano /usr/local/sbin/chatwoot-backup.sh
#!/usr/bin/env bash
set -euo pipefail

APP_DIR="/opt/chatwoot"
BACKUP_DIR="${APP_DIR}/backups"
STAMP="$(date +%F_%H-%M-%S)"
DUMP_FILE="${BACKUP_DIR}/chatwoot_${STAMP}.sql.gz"

set -a
source /etc/restic/chatwoot.env
set +a

mkdir -p "${BACKUP_DIR}"
cd "${APP_DIR}"

docker compose exec -T postgres \
  pg_dump -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" | gzip -9 > "${DUMP_FILE}"

restic backup \
  "${DUMP_FILE}" \
  "${APP_DIR}/.env" \
  "${APP_DIR}/compose.yaml" \
  "${APP_DIR}/Caddyfile" \
  "${APP_DIR}/storage" \
  "${APP_DIR}/caddy/data"

restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune

rm -f "${DUMP_FILE}"

The script needs PostgreSQL variables from /opt/chatwoot/.env. Add loading of this file before running pg_dump, otherwise the variables will not be defined in the script environment.

sudo sed -i '/cd "${APP_DIR}"/a set -a\nsource "${APP_DIR}/.env"\nset +a' \
  /usr/local/sbin/chatwoot-backup.sh

sudo chmod 700 /usr/local/sbin/chatwoot-backup.sh
sudo /usr/local/sbin/chatwoot-backup.sh

The first run initializes the restic repository if required, or prompts for confirmation. After successful completion, check the list of snapshots.

sudo bash -c 'source /etc/restic/chatwoot.env && restic snapshots'

Schedule Backups with cron

Run backups at night, for example at 03:30. Output is redirected to a log, which will be useful when investigating errors. Once a week, verify that a new snapshot has actually been created.

sudo crontab -e
30 3   * /usr/local/sbin/chatwoot-backup.sh >> /var/log/chatwoot-backup.log 2>&1

Recovery Testing

The presence of backup files does not prove that recovery is possible. At least once per quarter, restore the dump on a test server. To restore the database, stop the application, create an empty database, and load the SQL dump. Do not perform recovery in production without a confirmed rollback plan.

gunzip -c chatwoot_YYYY-MM-DD_HH-MM-SS.sql.gz | \
  docker compose exec -T postgres \
  psql -U chatwoot -d chatwoot_production

Updating Chatwoot and Containers

For a small instance, use a maintenance window: notify operators, create a backup, update the image, apply migrations, and check the logs. A zero-downtime rolling update requires multiple web replicas, shared storage, a separate database, and a load balancer; for a single VPS, it is usually unnecessarily complex.

cd /opt/chatwoot
sudo /usr/local/sbin/chatwoot-backup.sh
docker compose pull
docker compose run --rm rails bundle exec rails db:migrate
docker compose up -d
docker image prune -f
docker compose ps

Before updating, read the release notes for the target version. Pay particular attention to major releases, PostgreSQL/Redis requirements, and environment variable changes. If the update causes errors, do not delete volumes or run arbitrary migration commands: first save the logs and revert to the previous image.

Also monitor disk space and container status.

df -h
docker system df
docker compose ps
docker stats --no-stream

Troubleshooting and FAQ

Why does Caddy not issue an SSL certificate and show an ACME error in the logs?

First, check the domain A record with getent ahostsv4 chat.example.com: it must return your VPS IP address. Then make sure the firewall allows ports 80 and 443, and that no other process is using these ports: sudo ss -ltnp '( sport = :80 or sport = :443 )'. Also disable DNS record proxying through the CDN during initial diagnostics. Check docker compose logs caddy; it will contain the exact cause of the ACME failure.

Why does the Chatwoot page return 502 Bad Gateway?

A 502 code means that Caddy is running but cannot get a response from the rails container. Run docker compose ps and make sure rails has the Up status. Then read the latest logs: docker compose logs --tail=150 rails. Common causes include unapplied database migrations, an invalid SECRET_KEY_BASE, unavailable PostgreSQL, or insufficient RAM. Check memory with free -h and kernel OOM logs with dmesg -T | grep -i killed.

Why does the worker keep restarting?

The worker container depends on Redis and PostgreSQL, so first check their healthcheck: docker compose ps. Then open the logs with docker compose logs --tail=200 worker. A Redis connection error usually means an invalid REDIS_URL; inside the Docker network, use the redis service name rather than localhost. A database error indicates incorrect POSTGRES_HOST, password, or that the db:chatwoot_prepare task was not run.

Why does Chatwoot not send email invitations and notifications?

Email is sent by the Sidekiq worker, so open its logs and look for an SMTP error. Check the server address, port, login, application password, and encryption type. Port 587 usually requires SMTP_ENABLE_STARTTLS_AUTO=true and SMTP_AUTHENTICATION=plain. After changing .env, apply the settings with docker compose up -d --force-recreate rails worker. Keep in mind that the mail provider may block SMTP until the domain is verified or SMTP access is enabled.

Why do emails end up in spam?

The issue is usually not Chatwoot, but the reputation of the domain or SMTP service. The MAILER_SENDER_EMAIL address must match the verified sender domain. Configure SPF, DKIM, and DMARC through the DNS panel of your mail service. Do not use a random sender address if the SMTP provider permits sending only from a verified domain. Check the email headers in your mail client: they will show SPF and DKIM results. For transactional emails, it is better to use a dedicated SMTP service.

What is the minimum suitable VPS configuration?

For a small production instance, use at least 2 vCPU, 4 GB RAM, and 50 GB NVMe. A configuration with 2 GB of memory may work for testing, but updates, background attachment processing, and simultaneous conversations increase the risk of OOM errors. If you have multiple channels enabled, actively use the API, or store many files, start with 4 vCPU, 8 GB RAM, and 100 GB of disk space. Choose disk capacity with room for backups and attachments.

Which should I choose for this task: VPS or dedicated server?

A VPS suits most teams with up to several dozen operators: it is easier to launch, less expensive, and can be easily scaled by changing the plan. Choose a dedicated server for sustained high load, physical isolation requirements, a large volume of local attachments, or when several critical services are hosted on one machine. A dedicated server by itself does not solve backup, security, or monitoring issues. For a single Chatwoot instance, it is more practical to start with a VPS and move to a dedicated server after measuring actual load.

How can I safely change the Chatwoot domain?

First create a DNS record for the new domain, then change FRONTEND_URL in .env and the domain in Caddyfile. Restart the services: docker compose up -d --force-recreate rails worker caddy. Then check curl -I https://new-chat.example.com. It is advisable to temporarily keep the old domain in Caddy as a separate site with a redirect so that old links and already installed widgets do not stop working immediately.

Conclusions and Next Steps

You now have self-hosted Chatwoot on a VPS with Docker, PostgreSQL, Redis, HTTPS through Caddy, and SMTP for system email. The configuration isolates internal services from the internet, while backups make it possible to restore the database and files after a failure.

  1. Create an inbox for the website and connect the widget, then add operators through invitations.
  2. Configure SPF, DKIM, and DMARC, and test email delivery from a real client address.
  3. Set up monitoring for disk space, memory, HTTPS availability, and successful nightly backups.
  4. As load grows, move attachments to S3-compatible storage and PostgreSQL to a separate managed server or dedicated node.

Was this guide helpful?

Your feedback helps us improve our guides.

Share this post:

Send this guide to someone who may find it useful.

Telegram VKVK WhatsApp Facebook LinkedIn XX

Chatwoot installation on VPS: Docker, PostgreSQL, Redis, SSL, and email setup
support_agent
Valebyte Support
Usually replies within minutes
Hi there!
Send us a message and we'll reply as soon as possible.