Docker Compose self-host
- Written for
- + Written for
- Deprecated
- + Deprecated
- Applies to
- + Applies to
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 |
|---|---|---|
|
| Tested on Hetzner CX22 (€4.50/mo), OVH KS-LE-A, DigitalOcean Premium AMD. Anything with 4 vCPU + 8 GB + 80 GB SSD works. |
|
| Ubuntu 22.04 LTS or Debian 12 recommended. Any modern Linux with cgroups v2. |
|
| A record at the apex (or chosen base) pointing at the VPS IP. Add a wildcard CNAME ( |
|
| With docker compose v2 plugin. |
|
| 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 + RedisServices in docker-compose.prod.yml
Name | Type | Description |
|---|---|---|
|
| NestJS REST + GraphQL. Routed via Traefik to |
|
| Socket.IO at |
|
| BullMQ consumer. No public route. |
|
| LLM broker + RAG at |
|
| LiveKit + Twilio bridge at |
|
| Self-hosted WebRTC SFU + TURN. |
|
| PSTN ↔ LiveKit bridge. |
|
| React SPA served by nginx. |
|
| Next.js KB renderer. |
|
| This site. |
|
| nginx serving |
|
| 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 --buildTraefik (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 enableTLS
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 --buildMigrations 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/healthScaling out
When one box isn't enough:
Parameters
Name | Type | Description |
|---|---|---|
|
| Move Postgres + Redis to managed services (RDS, Aurora, ElastiCache, Cloud SQL). The single-VPS compose stops being the bottleneck. |
|
| Run |
|
| The Socket.IO Redis adapter is already configured; just run multiple |
|
| 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.examplefrom 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_userneedsGRANT ALL ON ALL TABLESon the schema. The bundledlivechat-migrate.shgrants 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.yamlvia the deploy script (which substitutes env vars) instead of using the template directly.