ChatAI Docs

Docker

Production Docker Compose for ChatAI, Postgres, volumes, and reverse proxies.

For production VPS deployment (Neon, Caddy, GHCR, app.nightzeros.com), see the VPS deployment guide in the repository.

Quick start

cp .env.example .env
# Set BETTER_AUTH_SECRET (openssl rand -base64 32) and AI_API_KEY

docker compose up --build

Open http://localhost:3000. The app:

  1. Waits for Postgres to be healthy
  2. Runs Drizzle migrations via packages/database/scripts/migrate.mjs
  3. Serves the Next.js app on port 3000
  4. Exposes GET /api/health for load balancers (200 when DB is reachable)

Services and volumes

Compose serviceRoleNamed volume
dbPostgres 17 + pgvectorchatai_pgdata
appChatAI web + API + ingest workerchatai_uploads

Uploads are stored under /app/uploads inside the container (UPLOAD_DIR). Keep both volumes on durable disks in production.

The Compose file overrides DATABASE_URL for the app service to postgresql://chatai:chatai@db:5432/chatai on the internal network. Your host .env DATABASE_URL is still used for local pnpm tools talking to published port 5432.

Healthchecks

  • db: pg_isready
  • app: wget → http://127.0.0.1:3000/api/health

Smoke after boot:

curl -s http://localhost:3000/api/health
# {"status":"ok","db":"ok"}

Public URL and auth

Set BETTER_AUTH_URL to the exact origin browsers use (scheme + host + port, no path):

BETTER_AUTH_URL=https://chatai.example.com

Widget embeds, data-api-url, and React apiUrl must use the same origin. Mismatches cause CORS failures or broken auth cookies.

Reverse proxy and TLS

Terminate TLS at nginx, Caddy, Traefik, or a cloud load balancer, then proxy to app:3000.

Checklist:

  • Forward Host and X-Forwarded-Proto (or equivalent) so the app sees HTTPS
  • Allow long-lived connections for SSE chat streams
  • Do not strip CORS headers on /api/v1/*
  • Keep WebSocket upgrade headers if your proxy requires them for streaming

Example Caddy snippet:

chatai.example.com {
  reverse_proxy localhost:3000
}

Voice in production

Voice keeps each live call's control link, meter and recording in the web process, so the runtime contract is strict:

  • Long-lived Node.js server only. The Docker image (node apps/web/server.js) and next start are supported. Serverless platforms (Vercel, AWS Lambda, Netlify) are not: Voice fails closed there and visitors see "Voice isn't available right now" while text chat keeps working.
  • One Voice replica, or sticky routing by Voice session id. A request that reaches the wrong replica is answered 421 and never touches the call.
  • Signals reach Node. The image sets NEXT_MANUAL_SIG_HANDLE=1 and its entrypoint forwards SIGTERM/SIGINT to Node. With a custom entrypoint, exec Node or forward the signal yourself.
  • Stop grace ≥ 15 s. On SIGTERM ChatAI stops accepting new calls, hangs up every live call, settles its usage once, finalizes its recording, then exits — within VOICE_SHUTDOWN_GRACE_MS (default 8 s). The bundled Compose files set stop_grace_period: 20s; set the equivalent on Kubernetes (terminationGracePeriodSeconds) or your platform.
  • Persistent spool. Keep VOICE_RECORDING_SPOOL_DIR on the uploads volume so a recording interrupted by a crash (or a drain that runs out of time) is recovered as a partial recording.

Visitors on a call during a deploy see the call end ("Voice disconnected. You can keep typing here.") and can keep typing or start a new call once the server is back; they are never shown deployment or infrastructure details. A hard kill (SIGKILL, OOM) skips the drain: the next process recovers the call when the visitor's heartbeat arrives, hangs it up and settles it from the provider or the last checkpoint.

Secrets

  • Generate a long BETTER_AUTH_SECRET (min 16 characters; prefer 32+ random bytes)
  • Never commit .env
  • Prefer a secrets manager or Compose secrets: in production instead of a world-readable env file

Optional local models

See Local models (Ollama) for the Compose local-models profile.

Demo data

After the app is healthy:

pnpm seed:demo
# demo@chatai.local / DemoPass123!

Docker-only convenience (dev/demo — do not use in production):

SEED_DEMO_ON_START=1 docker compose up --build

The entrypoint waits for /api/health, then runs the same idempotent seed script. Prefer pnpm seed:demo when developing against Neon or a long-lived container.