Skip to main content

Documentation Index

Fetch the complete documentation index at: https://chatbotx.io/docs/llms.txt

Use this file to discover all available pages before exploring further.

ChatbotX exposes three services that need to be proxied:
ServiceInternal portRecommended subdomain
Builder (web UI + API)3123app.yourdomain.com
PartySocket (WebSocket)1999ws.yourdomain.com
Storage (S3 API + assets)9000cdn.yourdomain.com
The steps below are written for Ubuntu / Debian. Package and path names differ slightly on RHEL-based distros (dnf instead of apt, /etc/nginx/conf.d/ instead of /etc/nginx/sites-available/).

Installation

1

Install Nginx and Certbot

sudo apt update
sudo apt install -y nginx certbot python3-certbot-nginx
2

Create the webroot directory

sudo mkdir -p /var/www/certbot
3

Create a temporary HTTP config

Create /etc/nginx/sites-available/chatbotx with a minimal config that lets Certbot complete the ACME challenge:
/etc/nginx/sites-available/chatbotx
server {
    listen 80;
    server_name app.yourdomain.com ws.yourdomain.com cdn.yourdomain.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }
}
Enable it and reload:
sudo ln -s /etc/nginx/sites-available/chatbotx /etc/nginx/sites-enabled/chatbotx
sudo nginx -t && sudo systemctl reload nginx
4

Obtain TLS certificates

sudo certbot certonly --webroot -w /var/www/certbot \
  -d app.yourdomain.com \
  -d ws.yourdomain.com \
  -d cdn.yourdomain.com \
  --email you@yourdomain.com \
  --agree-tos --non-interactive
Certificates are saved to /etc/letsencrypt/live/<domain>/.
5

Replace with the full HTTPS config

Overwrite /etc/nginx/sites-available/chatbotx with the configuration below.
# ── Builder ────────────────────────────────────────────────
server {
    listen 80;
    server_name app.yourdomain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name app.yourdomain.com;

    ssl_certificate     /etc/letsencrypt/live/app.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.yourdomain.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;

    client_max_body_size 100M;

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options nosniff always;
    add_header X-Frame-Options DENY always;

    location / {
        proxy_pass         http://localhost:3123;
        proxy_http_version 1.1;
        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;
    }
}

# ── PartySocket ────────────────────────────────────────────
server {
    listen 80;
    server_name ws.yourdomain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name ws.yourdomain.com;

    ssl_certificate     /etc/letsencrypt/live/ws.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ws.yourdomain.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;

    location / {
        proxy_pass          http://localhost:1999;
        proxy_http_version  1.1;
        proxy_set_header    Upgrade           $http_upgrade;
        proxy_set_header    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;
        proxy_send_timeout  3600s;
    }
}

# ── Storage ────────────────────────────────────────────────
server {
    listen 80;
    server_name cdn.yourdomain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name cdn.yourdomain.com;

    ssl_certificate     /etc/letsencrypt/live/cdn.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/cdn.yourdomain.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;

    client_max_body_size 1G;

    location / {
        proxy_pass         http://localhost:9000;
        proxy_http_version 1.1;
        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;
    }
}
6

Test and reload Nginx

sudo nginx -t && sudo systemctl reload nginx
7

Enable automatic certificate renewal

Certbot installs a systemd timer by default. Verify it is active:
sudo systemctl status certbot.timer
Add a reload hook so Nginx picks up renewed certificates automatically:
echo -e '#!/bin/sh\nsystemctl reload nginx' | sudo tee /etc/letsencrypt/renewal-hooks/post/reload-nginx.sh
sudo chmod +x /etc/letsencrypt/renewal-hooks/post/reload-nginx.sh

ChatbotX configuration

Update your .env to use the public HTTPS URLs and restart the services:
.env
NEXT_PUBLIC_BUILDER_URL=https://app.yourdomain.com
NEXT_PUBLIC_PARTYSOCKET_URL=https://ws.yourdomain.com
BETTER_AUTH_URL=https://app.yourdomain.com
NEXT_PUBLIC_ASSET_URL=https://cdn.yourdomain.com/chatbotx/public/
S3_ENDPOINT=https://cdn.yourdomain.com
NEXT_PUBLIC_BUILDER_URL and BETTER_AUTH_URL must match exactly. A mismatch causes authentication callbacks to fail.

WebSocket support

PartySocket requires the Upgrade and Connection headers to be forwarded. The secure config above includes these on the ws.yourdomain.com server block. The extended proxy_read_timeout and proxy_send_timeout values (3600s) prevent Nginx from closing long-lived WebSocket connections.

Storage (RustFS / S3)

The storage service runs on port 9000. Proxying it through Nginx gives you HTTPS asset URLs. Set client_max_body_size to a value larger than your largest expected upload — the example uses 1G.
The storage console (port 9001) does not need to be publicly exposed. Access it directly on the server via an SSH tunnel or restrict it to a local interface.