API Docs

Docker Compose self-host

AdminUpdated Sep 19, 2026

Docker Compose self-host

Run all of Chatly on a single VPS with one compose file — from clone to live install in 20 minutes.

The simplest production deployment of Chatly: one VPS, one docker-compose.prod.yml file. Handles ~5,000 workspaces / ~50,000 monthly active conversations on a 4-core 8 GB box. Beyond that, scale out to Kubernetes via Helm.

Info — Reference deployment is at our dogfood VPS

This is exactly how we run our own demo + reference deployment. The same compose file, the same volumes, the same Traefik config. If it works for us, it works for you.

Prerequisites

Hardware + software

Name

Type

Description

VPS

public IPv4

Tested on Hetzner CX22 (€4.50/mo), OVH KS-LE-A, DigitalOcean Premium AMD. Anything with 4 vCPU + 8 GB + 80 GB SSD works.

OS

Linux

Ubuntu 22.04 LTS or Debian 12 recommended. Any modern Linux with cgroups v2.

Domain

DNS

A record at the apex (or chosen base) pointing at the VPS IP. Add a wildcard CNAME (*.your-chatly.example → your-chatly.example) so subdomain routing just works.

Docker

24+

With docker compose v2 plugin. docker compose (no dash), not the legacy docker-compose.

Open ports

firewall

See Firewall below.

For a true production deployment you'll want managed Postgres + Redis (RDS, Aurora, Cloud SQL, ElastiCache, Upstash, Render). The compose file optionally runs them locally, but that's a dev-grade deployment — fine for evaluation, risky for production data.

Services

                            ┌──────────────┐
                            │   Traefik    │  ← 80, 443
                            └─────┬────────┘
                                  │
        ┌─────────┬───────────┬───┴────────┬──────────────┐
        │         │           │            │              │
   marketing  dashboard   help-center     api          widget-host
        │         │           │            │              │
        │         │           │            ├──► realtime
        │         │           │            ├──► ai
        │         │           │            ├──► worker
        │         │           │            └──► voice ─► livekit + livekit-sip
        │         │           │
        └─────────┴───────────┴────► Postgres + Redis

Services in docker-compose.prod.yml

Name

Type

Description

api

Dockerfile.api

NestJS REST + GraphQL. Routed via Traefik to api.{host}.

realtime

Dockerfile.realtime

Socket.IO at ws.{host}. Sticky sessions via Redis adapter.

worker

Dockerfile.worker

BullMQ consumer. No public route.

ai

Dockerfile.ai

LLM broker + RAG at ai.{host}.

voice

Dockerfile.voice

LiveKit + Twilio bridge at voice.{host}.

livekit

livekit/livekit-server:v1.7

Self-hosted WebRTC SFU + TURN.

livekit-sip

livekit/sip:latest

PSTN ↔ LiveKit bridge.

dashboard

Dockerfile.dashboard

React SPA served by nginx. app.{host}.

help-center

Dockerfile.help-center

Next.js KB renderer. help.{host}.

marketing

Dockerfile.marketing

This site. {host}.

widget-host

Dockerfile.widget-host

nginx serving /loader.js + /widget.js. widget.{host}.

postgres + redis

optional

Bundled for dev / single-VPS evals. Use managed services in production.

Bring-up (clone → live)

# 1. Clone the repo on the VPS
git clone [email protected]:livechat/platform.git /srv/livechat
cd /srv/livechat

# 2. Generate sticky operator secrets (one-time)
openssl genrsa -out /root/livechat-jwt.pem 2048
openssl rsa -pubout -in /root/livechat-jwt.pem -out /root/livechat-jwt.pub
openssl rand -base64 32 > /root/livechat-mfa.key       # MFA_ENCRYPTION_KEY
openssl rand -base64 32 > /root/livechat-admin.key     # ADMIN_API_KEY
chmod 600 /root/livechat-*.{key,pem}

# 3. Write the env file (see selfhost-env doc for the full list)
cat > .env.prod <<ENV
NODE_ENV=production
DATABASE_URL=postgresql://livechat_user:$DB_PWD@pg-host:5432/livechat_prod
REDIS_URL=redis://default:$RD_PWD@redis-host:6379/0
JWT_PRIVATE_KEY_PEM_B64=$(base64 -w0 /root/livechat-jwt.pem)
JWT_PUBLIC_KEY_PEM_B64=$(base64 -w0 /root/livechat-jwt.pub)
MFA_ENCRYPTION_KEY=$(cat /root/livechat-mfa.key)
ADMIN_API_KEY=$(cat /root/livechat-admin.key)
CORS_ORIGINS=https://app.chatly.example,https://chatly.example
API_HOST_FQDN=api.chatly.example
DASHBOARD_HOST_FQDN=app.chatly.example
HELP_HOST_FQDN=help.chatly.example
MARKETING_HOST_FQDN=chatly.example
WIDGET_HOST_FQDN=widget.chatly.example
REALTIME_HOST_FQDN=ws.chatly.example
VOICE_HOST_FQDN=voice.chatly.example
LIVEKIT_HOST_FQDN=livekit.chatly.example
LIVEKIT_API_KEY=API$(openssl rand -hex 4)
LIVEKIT_API_SECRET=$(openssl rand -base64 48)

# Internal service-to-service secrets. Both are REQUIRED: the AI service and
# the worker refuse to start without them. They are not customer-facing and
# never leave your network — they are what stops one of your own services
# from being callable by anyone who can reach it.
INTERNAL_API_TOKEN=$(openssl rand -base64 48)
INTERNAL_WORKER_SECRET=$(openssl rand -base64 48)
ENV

# 4. Run migrations (one-shot)
docker compose -f docker-compose.prod.yml --env-file .env.prod \
  run --rm api node ./node_modules/.bin/drizzle-kit migrate

# 5. Bring everything up
docker compose -f docker-compose.prod.yml --env-file .env.prod \
  up -d --build

Traefik (bundled, or external) issues Let's Encrypt certs on first request to each subdomain. Allow ~30 seconds after bring-up for the first cert to provision.

DNS records

A     chatly.example       → <vps-ipv4>
CNAME *.chatly.example     → chatly.example.

The wildcard CNAME is what lets app., api., ws., help., voice., livekit., widget. all resolve to the same VPS. If your DNS provider doesn't support wildcard CNAMEs, add an A record per subdomain.

Firewall

22/TCP      — SSH (restrict to your office IPs)
80/TCP      — HTTP (ACME challenge + redirect to 443)
443/TCP     — HTTPS (the actual app traffic)

50000-50100/UDP — LiveKit RTC media
7881/TCP        — LiveKit TLS fallback for restrictive networks
3478/UDP, 5349/TCP — TURN STUN

5060/UDP, 5060/TCP — SIP signaling
50000-50100/UDP    — SIP RTP media (shares LiveKit's range — they cooperate)

UFW one-liner:

ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 80,443/tcp
ufw allow 7881/tcp
ufw allow 5060/tcp
ufw allow 50000:50100/udp
ufw allow 5060/udp
ufw allow 3478/udp
ufw allow 5349/tcp
ufw enable

TLS

Traefik with the bundled config issues Let's Encrypt certs via HTTP-01 challenges. The relevant resolver in docker-compose.prod.yml:

- "[email protected]"
- "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"
- "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"

For wildcard cert support (e.g. *.acme.com), swap to DNS-01 with your DNS provider's plugin — see the Traefik docs.

Updates

To pull a new release:

cd /srv/livechat
git fetch origin && git checkout main && git pull
docker compose -f docker-compose.prod.yml --env-file .env.prod \
  run --rm api node ./node_modules/.bin/drizzle-kit migrate
docker compose -f docker-compose.prod.yml --env-file .env.prod \
  up -d --build

Migrations are forward-only; we never edit historical migrations. Roll-back of a release is a code-revert + redeploy.

Health checks

Each service exposes /health returning 200 when healthy. Traefik won't route to unhealthy services. The dashboard at https://app.chatly.example/_status shows the full health matrix.

For your own monitoring:

curl -fsSL https://api.chatly.example/health
curl -fsSL https://ws.chatly.example/health
curl -fsSL https://app.chatly.example/health

Scaling out

When one box isn't enough:

Parameters

Name

Type

Description

Step 1

externalize stateful

Move Postgres + Redis to managed services (RDS, Aurora, ElastiCache, Cloud SQL). The single-VPS compose stops being the bottleneck.

Step 2

split by CPU

Run ai + worker on a second VPS first — they're the most CPU-hungry.

Step 3

scale realtime

The Socket.IO Redis adapter is already configured; just run multiple realtime replicas behind a sticky-session load balancer.

Step 4

move to Helm

At ~30k MAU you'll want autoscaling, rolling updates, and proper HA. See Helm chart.

Troubleshooting

Warning — Cert issuance fails repeatedly

Almost always a DNS issue — Let's Encrypt requires HTTP-01 to resolve the hostname back to your VPS. Run dig api.chatly.example from any machine and verify it returns your VPS IP. Wildcard CNAME not active yet? Add an explicit A record.

Info — Migrations fail with 'permission denied'

The app role livechat_user needs GRANT ALL ON ALL TABLES on the schema. The bundled livechat-migrate.sh grants these after each migration; if you're not using it, run the grants manually post-migrate.

Info — LiveKit can't connect to Redis

LiveKit's Redis config requires explicit username + password interpolation. Generate livekit.runtime.yaml via the deploy script (which substitutes env vars) instead of using the template directly.

Was this page helpful?
Docker Compose self-host