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 --buildOpen http://localhost:3000. The app:
- Waits for Postgres to be healthy
- Runs Drizzle migrations via
packages/database/scripts/migrate.mjs - Serves the Next.js app on port
3000 - Exposes
GET /api/healthfor load balancers (200when DB is reachable)
Services and volumes
| Compose service | Role | Named volume |
|---|---|---|
db | Postgres 17 + pgvector | chatai_pgdata |
app | ChatAI web + API + ingest worker | chatai_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.comWidget 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
HostandX-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) andnext startare 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
421and never touches the call. - Signals reach Node. The image sets
NEXT_MANUAL_SIG_HANDLE=1and its entrypoint forwardsSIGTERM/SIGINTto Node. With a custom entrypoint,execNode or forward the signal yourself. - Stop grace ≥ 15 s. On
SIGTERMChatAI stops accepting new calls, hangs up every live call, settles its usage once, finalizes its recording, then exits — withinVOICE_SHUTDOWN_GRACE_MS(default 8 s). The bundled Compose files setstop_grace_period: 20s; set the equivalent on Kubernetes (terminationGracePeriodSeconds) or your platform. - Persistent spool. Keep
VOICE_RECORDING_SPOOL_DIRon 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 --buildThe 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.