Deployment
Simmetric Chat is air-gap capable and local-first. Five deployment targets: multi-container Docker Compose (recommended for production), a single all-in-one container (air-gapped environments), a Coolify-managed stack, dev overrides (hot-reload on Docker…
Simmetric Chat is air-gap capable and local-first. Five deployment targets: multi-container Docker Compose (recommended for production), a single all-in-one container (air-gapped environments), a Coolify-managed stack, dev overrides (hot-reload on Docker), and the Tauri desktop app. All deployment artifacts live in docker/; the desktop shell in src-tauri/. Images published to GHCR are amd64-only (linux/amd64 - QEMU-emulated arm64 builds hung; on arm64, self-build locally on native hardware). Every container uses wget healthchecks (--spider): server GET /api/health, collector GET /api/health on 3210, widget GET /health on 3211, Postgres pg_isready, Redis redis-cli ping.
Prerequisites
- Node.js ≥ 24, pnpm 12.8.1 (pinned), PostgreSQL 16.
- For local embeddings: ~2 GB RAM for
all-MiniLM-L6-v2(Xenova, CPU). - For local LLMs: Ollama with pre-pulled model(s) (
gemma4:latestdefault). - For vision OCR: Ollama with a vision model (
glm-ocr:latestdefault).
Docker Compose (production)
The production compose (docker/docker-compose.yml) defines 7 active services: frontend (nginx, :80/:443), server (:3000), collector (:3210), widget (:3211), postgres (16, image pgvector/pgvector:pg16), redis (7-alpine, optional scaling cache), and `ollama` on by default (owner decision 2026-09-08 - the provider «fetch failed» error path and the Ollama Cloud login require a REAL ollama container on the compose network; its own ollama-data model volume, models pulled via the app UI or docker exec). The `llmster` service (LM Studio, lmstudio/llmster-preview:cpu) exists but is profile-gated (profiles: [lmstudio]). The qdrant service no longer exists in the compose (vector = LanceDB by default; VECTOR_DB_PROVIDER=qdrant remains a code-level remote provider).
cp .env.example .env
# Edit .env: JWT_SECRET, COLLECTOR_SECRET, WIDGET_API_KEY, DATABASE_URL, LICENSE_KEY (if Enterprise)
docker compose -f docker/docker-compose.yml up -dCritical production env (see 03 - Configuration):
JWT_SECRET- required, cryptographic material (derivesENCRYPTION_KEYwhen unset).COLLECTOR_SECRET- required, must match between server and collector.WIDGET_API_KEY- required for the widget, must match between widget and server.DATABASE_URL- PostgreSQL 16.LICENSE_KEY- Enterprise only (JWT RS256; the public key is embedded in the server source; the no-.env alternative = the admin-UI paste /PUT /api/license/instance).ENCRYPTION_KEY(base64 32 bytes) - recommended for data-at-rest persistence beyondJWT_SECRETrotations.
Key rotation: set LEGACY_PREVIOUS_ENCRYPTION_KEYS (CSV of base64 32-byte keys)to keep old backups/data decryptable during rotation. There is no automatic
re-encryption job: manual decrypt-old → re-encrypt-new procedure.
Key auto-provisioning (server entrypoint)
The production entrypoint (docker/entrypoint-server.sh) runs on every boot: provisioning of ENCRYPTION_KEY and API_KEY_HMAC_SECRET with precedence operator-supplied value > value persisted in the server-storage volume > generated once and persisted (never regenerated over existing material); then prisma migrate deploy + seed and the server start. Dev configurations (docker-compose.dev.yml) skip provisioning and keep the scrypt fallback.
Volumes & persistence
- PostgreSQL: dedicated volume (
pgdata). - LanceDB:
${STORAGE_PATH}/vectors/lancedb(collector). - File storage:
${STORAGE_PATH}(collector, documents/avatars/uploads). - Backups: destinations configured per job (disk/S3/Dropbox/...).
Post-deploy health check
Containers self-check via wget (see the intro). Equivalent manual checks:
curl http://localhost:3000/api/health
# { "status": "ok", "checks": { "database": "ok", "collector": "ok", "disk": "ok" } }
curl http://localhost:3210/api/health # collector
curl http://localhost:3211/health # widgetSingle-container all-in-one
docker/Dockerfile builds a single image supervised by supervisord running PostgreSQL 16 (pgvector compiled offline for air-gap), the server, the collector, and the widget. It exposes 3000/3210/3211, uses a localhost DATABASE_URL, and healthchecks /api/health. Built for air-gapped/offline/demo environments: the default env values are placeholders - override them via .env or -e.
Coolify
docker/docker-compose.coolify.yml + docker/nginx.coolify.conf automate image builds, internal networking, volumes, healthchecks, and the enterprise plugin mount (absolute host path ${ENTERPRISE_PLUGIN_PATH:-/opt/simmetric-enterprise}). Required secrets use the :? syntax (Coolify refuses to deploy when missing): JWT_SECRET, COLLECTOR_SECRET, WIDGET_API_KEY, POSTGRES_PASSWORD, APP_URL, ALLOWED_ORIGINS. Differences from the base compose: no container_name, no host ports on app services (ingress owned by Traefik, HTTP-only - Traefik terminates TLS), and ENCRYPTION_KEY/API_KEY_HMAC_SECRET deliberately not interpolated (auto-provisioned into the volume).
Local development
pnpm install
pnpm db:generate # Prisma client (+ Prisma/pnpm symlink fix)
pnpm --filter server db:migrate # apply migrations
pnpm dev # turborepo: server + collector + widget + frontend in parallelDetails in 10 - Development.
Dev overrides (Docker)
docker/docker-compose.dev.yml mounts sources on top of the production compose for hot-reload (NODE_ENV=development). docker-compose.infra.yml starts only Postgres/Qdrant with the app running on the host.
Desktop (Tauri)
src-tauri/ - desktop app wrapping the frontend. Build with pnpm tauri build. Configure src-tauri/tauri.conf.json for a remote server URL or a local bundle.
Reverse proxy (Nginx)
Key points:
- SSE: disable buffering (
proxy_buffering off;,X-Accel-Buffering: noalready sent by the server/widget). - Timeouts: slow local LLMs → high
proxy_read_timeout(≥ 600s, the agent watchdog default is 10 min). - Uploads:
client_max_body_size≥ 100 MB (multer document limit). - CORS: the allowlist is app-managed (
ALLOWED_ORIGINS); the widget has dynamic per-origin CORS (allowedOrigins).
Production backups
- Create a destination (
POST /api/backup-destinations). - Create a job (
POST /api/backup-jobs) with cronfrequency/time+retentionDays. - Verify with
POST /backup-destinations/:id/testand a manual runPOST /backup-jobs/:id/run. - Restore:
POST /api/backups/restore/:logId(dry-run recommended first:/restore/:logId/dry-run). Atomicity via psql--single-transaction.
See 05 - Feature Guide § Backup.
Production security
- Change the bootstrap admin password; set
ALLOW_REGISTRATION=falsein prod (self-registration closed). DLP_ENABLEDdefaults to"true"in v0.22 - input/output chat redaction over 10 built-in patterns (see 05 - Feature Guide); useDLP_BYPASS_ROLESto exempt selected roles.- Enable
audit_log_immutable(Enterprise) for append-only audit. - HTTPS terminated at the reverse proxy;
helmetheaders active on the server (relaxed on the widget for embedding). - Rate limits are pre-configured (see 04 - API Reference § Rate limit): the chat limit is now enforced by the agent's budget tracker (per-user concurrency, wallclock, tokens) plus the global 200 req/min safety net.
v0.28.1 Provenance
| Statement | Source |
|---|---|
| 7 active services + ollama on by default + the profile-gated llmster + qdrant gone | docker/docker-compose.yml (row-time probe 35-02) |
| pnpm 12.8.1 | docs/DEVELOPMENT.md (Prerequisites, row-time) |
Operational reference - Simmetric Chat v0.28.1 (docs updated 2026-10-02, census SHA `aca5b8ff').
Cross-links
- Full env reference: 03 - Configuration
- Development: 10 - Development
- Deployment troubleshooting: 12 - Troubleshooting