mirror of
https://github.com/dartdavros/chatballs.git
synced 2026-10-05 09:14:58 +03:00
Open source: «развернуть за минуту». Никаких параметров в .env и CLI — при пустом инстансе браузер показывает мастер (/api/v1/setup/): название организации, имя, e-mail, пароль владельца и флаг «Установить демо-данные». Запись идёт на соединении platform (роль app не создаёт организации) через tenancy/routing.use_database; после первого владельца мастер закрыт (409). bootstrap_owner и захардкоженные Edevs/Котова из установки удалены (bootstrap_edevs_owner остаётся тестовым helper'ом). Демо ставится в организацию установщика; реестр DemoRecord (post_save во время сида) даёт точное удаление в обратном порядке с разрывом PROTECT-циклов только между демо-объектами. Установка/удаление — outbox → worker; статус и витринные учётки (админ + сотрудники разных групп, пароль Chatbolls-Demo-2026) — карточка «Демо-данные» в «Настройках» (/company/demo/). Сид переписан под дизайн-базлайн v2 («Ателье Норд»): 6 сотрудников (группы, блокировка, TOTP, приглашения), 4 агента (активные/черновик/выключенный), подключения TG/MAX/почта/два веб-виджета (анонимный и авторизованный) с ошибкой у одного, знания с иерархией категорий и вложениями (md/txt/pdf), 30 дней истории LLM, 13 диалогов во всех состояниях с метками, приоритетами, заметками, ответственными, историей контакта, спамом и архивом, веб-гость через настоящую сессию виджета, шаблоны «/», портал поддержки со статьями (опубликованные/черновик/архив, вторая ревизия, оценки), звонки с метриками и приглашением, уведомления всех типов, привязки уведомителя. Аватары контактов — портреты владельца, публичный /api/v1/demo-media/. Голосовые — слоты под клипы владельца (media/voice/README.md). Тест покрытия: каждая модель hub_platform получает демо-запись; удаление возвращает счётчики к исходным. compose-профиль demo-seed и seed-demo.ps1 удалены; seed_demo --organization --apply/--remove для разработки; start.sh для Linux/macOS; README — раздел «Быстрый старт». Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
270 lines
12 KiB
YAML
270 lines
12 KiB
YAML
# Канонический production-манифест CustoCRM (ADR-HUB-0028).
|
||
# Один Compose для: box-установки клиента, hub.edevs.tech, staging, smoke.
|
||
# Локальная разработка — через override compose.dev.yaml; этот файл не содержит
|
||
# dev source mounts, dev-команд и host-портов (кроме public 80/443 у gateway).
|
||
#
|
||
# Image references берутся из release.env (immutable digest refs, см. ADR §release).
|
||
# В dev они не задаются — срабатывают безопасные defaults, а override подменяет
|
||
# image на build.
|
||
#
|
||
# Запуск production/box:
|
||
# docker compose --env-file .env --env-file release.env up -d
|
||
# Запуск разработки:
|
||
# docker compose -f compose.yaml -f compose.dev.yaml --env-file .env.example --env-file .env up
|
||
|
||
services:
|
||
postgres:
|
||
image: ${CUSTOCRM_POSTGRES_IMAGE:-pgvector/pgvector:pg16}
|
||
restart: unless-stopped
|
||
environment:
|
||
POSTGRES_DB: ${POSTGRES_DB:?POSTGRES_DB is required}
|
||
POSTGRES_USER: ${POSTGRES_USER:?POSTGRES_USER is required}
|
||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
|
||
POSTGRES_APP_USER: ${POSTGRES_APP_USER:?POSTGRES_APP_USER is required}
|
||
POSTGRES_APP_PASSWORD: ${POSTGRES_APP_PASSWORD:?POSTGRES_APP_PASSWORD is required}
|
||
POSTGRES_PLATFORM_USER: ${POSTGRES_PLATFORM_USER:?POSTGRES_PLATFORM_USER is required}
|
||
POSTGRES_PLATFORM_PASSWORD: ${POSTGRES_PLATFORM_PASSWORD:?POSTGRES_PLATFORM_PASSWORD is required}
|
||
POSTGRES_MIGRATION_USER: ${POSTGRES_MIGRATION_USER:?POSTGRES_MIGRATION_USER is required}
|
||
POSTGRES_MIGRATION_PASSWORD: ${POSTGRES_MIGRATION_PASSWORD:?POSTGRES_MIGRATION_PASSWORD is required}
|
||
volumes:
|
||
- ${CUSTOCRM_INSTANCE_DIR:-.}/data/postgres:/var/lib/postgresql/data
|
||
- ${CUSTOCRM_RELEASE_DIR:-.}/deploy/postgres/init-runtime-roles.sh:/docker-entrypoint-initdb.d/20-custocrm-runtime-roles.sh:ro
|
||
- ${CUSTOCRM_RELEASE_DIR:-.}/deploy/postgres/reassign-schema-ownership.sql:/custocrm-reassign-ownership.sql:ro
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 10
|
||
|
||
redis:
|
||
image: ${CUSTOCRM_REDIS_IMAGE:-redis:7-alpine}
|
||
restart: unless-stopped
|
||
command: ["redis-server", "--appendonly", "yes"]
|
||
volumes:
|
||
- ${CUSTOCRM_INSTANCE_DIR:-.}/data/redis:/data
|
||
healthcheck:
|
||
test: ["CMD", "redis-cli", "ping"]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 10
|
||
start_period: 20s
|
||
|
||
# One-shot init: только миграции (ADR-HUB-0028 §one-shot init). Владельца и
|
||
# демо-данные создаёт мастер первого запуска в браузере (/api/v1/setup/).
|
||
# collectstatic испечён в backend-образ, здесь не вызывается.
|
||
init:
|
||
image: ${CUSTOCRM_BACKEND_IMAGE:-custocrm-backend:dev}
|
||
env_file:
|
||
- ${CUSTOCRM_INSTANCE_DIR:-.}/.env
|
||
command: ["python", "manage.py", "migrate", "--noinput"]
|
||
environment:
|
||
CUS_DB_ROLE: migration
|
||
restart: "no"
|
||
depends_on:
|
||
postgres:
|
||
condition: service_healthy
|
||
redis:
|
||
condition: service_healthy
|
||
|
||
backend-app:
|
||
image: ${CUSTOCRM_BACKEND_IMAGE:-custocrm-backend:dev}
|
||
restart: unless-stopped
|
||
env_file:
|
||
- ${CUSTOCRM_INSTANCE_DIR:-.}/.env
|
||
command: >
|
||
sh -c "gunicorn hub_backend.asgi_app:application
|
||
--worker-class uvicorn.workers.UvicornWorker
|
||
--bind 0.0.0.0:8000
|
||
--workers $${CUS_GUNICORN_WORKERS:-3}
|
||
--timeout $${CUS_GUNICORN_TIMEOUT:-60}"
|
||
environment:
|
||
CUS_DB_ROLE: app
|
||
volumes:
|
||
# Legacy source media retained for the separately approved copy/hash
|
||
# migration. Production writes use the required S3 backend in C04.
|
||
- ${CUSTOCRM_INSTANCE_DIR:-.}/data/media:/app/apps/backend/media
|
||
depends_on:
|
||
init:
|
||
condition: service_completed_successfully
|
||
postgres:
|
||
condition: service_healthy
|
||
redis:
|
||
condition: service_healthy
|
||
healthcheck:
|
||
test:
|
||
[
|
||
"CMD",
|
||
"python",
|
||
"-c",
|
||
"import os, urllib.request; req = urllib.request.Request('http://127.0.0.1:8000/api/v1/health/live/', headers={'Host': os.environ.get('CUSTOCRM_APP_HEALTHCHECK_HOST', 'app.localhost'), 'X-Forwarded-Proto': 'https'}); urllib.request.urlopen(req, timeout=3)",
|
||
]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 10
|
||
start_period: 20s
|
||
|
||
backend-platform:
|
||
image: ${CUSTOCRM_BACKEND_IMAGE:-custocrm-backend:dev}
|
||
restart: unless-stopped
|
||
env_file:
|
||
- ${CUSTOCRM_INSTANCE_DIR:-.}/.env
|
||
command: >
|
||
sh -c "gunicorn hub_backend.asgi_platform:application
|
||
--worker-class uvicorn.workers.UvicornWorker
|
||
--bind 0.0.0.0:8000
|
||
--workers $${CUS_GUNICORN_WORKERS:-3}
|
||
--timeout $${CUS_GUNICORN_TIMEOUT:-60}"
|
||
environment:
|
||
CUS_DB_ROLE: platform
|
||
depends_on:
|
||
init:
|
||
condition: service_completed_successfully
|
||
postgres:
|
||
condition: service_healthy
|
||
redis:
|
||
condition: service_healthy
|
||
healthcheck:
|
||
test:
|
||
[
|
||
"CMD",
|
||
"python",
|
||
"-c",
|
||
"import os, urllib.request; req = urllib.request.Request('http://127.0.0.1:8000/api/v1/health/live/', headers={'Host': os.environ.get('CUSTOCRM_PLATFORM_HEALTHCHECK_HOST', 'platform.localhost'), 'X-Forwarded-Proto': 'https'}); urllib.request.urlopen(req, timeout=3)",
|
||
]
|
||
interval: 10s
|
||
timeout: 5s
|
||
retries: 10
|
||
start_period: 20s
|
||
|
||
# Django admin не подключён к gateway. Единственный host bind — loopback;
|
||
# удалённый доступ допускается только через SSH tunnel (ADR-HUB-0031).
|
||
backend-admin:
|
||
image: ${CUSTOCRM_BACKEND_IMAGE:-custocrm-backend:dev}
|
||
restart: unless-stopped
|
||
env_file:
|
||
- ${CUSTOCRM_INSTANCE_DIR:-.}/.env
|
||
command: >
|
||
sh -c "gunicorn hub_backend.asgi_admin:application
|
||
--worker-class uvicorn.workers.UvicornWorker
|
||
--bind 0.0.0.0:8000
|
||
--workers $${CUS_GUNICORN_WORKERS:-2}
|
||
--timeout $${CUS_GUNICORN_TIMEOUT:-60}"
|
||
# Break-glass technical surface only: schema credentials are never used by
|
||
# public app/platform runtimes and this service remains loopback-only.
|
||
environment:
|
||
CUS_DB_ROLE: migration
|
||
ports:
|
||
- "127.0.0.1:${CUSTOCRM_ADMIN_PORT:-18001}:8000"
|
||
depends_on:
|
||
init:
|
||
condition: service_completed_successfully
|
||
postgres:
|
||
condition: service_healthy
|
||
redis:
|
||
condition: service_healthy
|
||
|
||
worker:
|
||
image: ${CUSTOCRM_BACKEND_IMAGE:-custocrm-backend:dev}
|
||
restart: unless-stopped
|
||
env_file:
|
||
- ${CUSTOCRM_INSTANCE_DIR:-.}/.env
|
||
command: ["python", "manage.py", "run_worker"]
|
||
environment:
|
||
CUS_DB_ROLE: app
|
||
volumes:
|
||
- ${CUSTOCRM_INSTANCE_DIR:-.}/data/media:/app/apps/backend/media
|
||
depends_on:
|
||
backend-app:
|
||
condition: service_healthy
|
||
|
||
# Frontend: один nginx-образ со static-сборкой internal-ui и web-chat,
|
||
# внутренняя маршрутизация (/api/, /ws/, /chat-widget.js,
|
||
# /chat/, /calls/) — в deploy/nginx/frontend.production.conf. Не публикует
|
||
# host-порт: public boundary — gateway (Caddy).
|
||
frontend:
|
||
image: ${CUSTOCRM_FRONTEND_IMAGE:-custocrm-frontend:dev}
|
||
restart: unless-stopped
|
||
depends_on:
|
||
backend-app:
|
||
condition: service_healthy
|
||
|
||
# Gateway: единственный HTTP/HTTPS public boundary (ADR-HUB-0028 §gateway).
|
||
# Caddy: TLS termination, ACME, HTTP->HTTPS redirect, WebSocket upgrade (native).
|
||
# Маршрутизация публичных путей делегируется frontend (internal). Coturn не
|
||
# проксируется через Caddy — отдельная network boundary (profile calls).
|
||
gateway:
|
||
image: ${CUSTOCRM_GATEWAY_IMAGE:-caddy:2.8.4}
|
||
restart: unless-stopped
|
||
environment:
|
||
CUSTOCRM_APP_DOMAIN: ${CUSTOCRM_APP_DOMAIN:?CUSTOCRM_APP_DOMAIN is required}
|
||
CUSTOCRM_PLATFORM_DOMAIN: ${CUSTOCRM_PLATFORM_DOMAIN:?CUSTOCRM_PLATFORM_DOMAIN is required}
|
||
CUSTOCRM_ACME_EMAIL: ${CUSTOCRM_ACME_EMAIL:?CUSTOCRM_ACME_EMAIL is required}
|
||
ports:
|
||
- "${CUSTOCRM_WEB_LISTENING_IP:-0.0.0.0}:80:80"
|
||
- "${CUSTOCRM_WEB_LISTENING_IP:-0.0.0.0}:443:443"
|
||
volumes:
|
||
- ${CUSTOCRM_RELEASE_DIR:-.}/Caddyfile:/etc/caddy/Caddyfile:ro
|
||
- ${CUSTOCRM_INSTANCE_DIR:-.}/data/caddy:/data
|
||
- ${CUSTOCRM_INSTANCE_DIR:-.}/data/caddy-config:/config
|
||
depends_on:
|
||
frontend:
|
||
condition: service_started
|
||
backend-app:
|
||
condition: service_healthy
|
||
backend-platform:
|
||
condition: service_healthy
|
||
|
||
# Coturn — медиа-relay для TURN fallback (SPEC-HUB-0013 §11).
|
||
# Опциональный profile `calls` (ADR-HUB-0028): включается через COMPOSE_PROFILES=calls.
|
||
# Host networking: relay использует широкий UDP-диапазон и реальный внешний IP,
|
||
# публикуется напрямую и не проходит через HTTP reverse proxy. credentials
|
||
# выдаёт backend по общему CUS_CALL_TURN_SECRET (static-auth-secret).
|
||
coturn:
|
||
profiles: ["calls"]
|
||
image: ${CUSTOCRM_COTURN_IMAGE:-coturn/coturn:4.6}
|
||
restart: unless-stopped
|
||
network_mode: host
|
||
command:
|
||
- -n
|
||
- --log-file=stdout
|
||
- --no-cli
|
||
- --fingerprint
|
||
- --use-auth-secret
|
||
- --static-auth-secret=${CUS_CALL_TURN_SECRET}
|
||
- --realm=${CUS_CALL_TURN_REALM}
|
||
# Весь TURN живёт на выделенном публичном IP (отдельный порт/NIC), Caddy — на
|
||
# основном IP. Это освобождает 443 под TURN-over-TLS без конфликта с web.
|
||
- --listening-ip=${CUS_TURN_LISTENING_IP}
|
||
- --relay-ip=${CUS_TURN_EXTERNAL_IP}
|
||
- --external-ip=${CUS_TURN_EXTERNAL_IP}
|
||
- --listening-port=${CUS_TURN_LISTENING_PORT:-3478}
|
||
# TURN-over-TLS на 443: проходит через VPN/строгие сети, где UDP и 3478 режут.
|
||
- --tls-listening-port=${CUS_TURN_TLS_PORT:-443}
|
||
- --cert=/etc/coturn/certs/fullchain.pem
|
||
- --pkey=/etc/coturn/certs/privkey.pem
|
||
- --min-port=${CUS_TURN_MIN_PORT:-49160}
|
||
# Relay-диапазон UDP (1 порт на media-endpoint; ~2 порта на звонок).
|
||
# 49160-49999 = 840 портов ≈ 420 одновременных TURN-звонков на хосте.
|
||
# Расширяется через CUS_TURN_MIN_PORT/CUS_TURN_MAX_PORT; все порты диапазона
|
||
# должны быть открыты на firewall и не пересекаться с ephemeral-диапазоном ОС.
|
||
- --max-port=${CUS_TURN_MAX_PORT:-49999}
|
||
# Запрет анонимного и внутрисетевого relay (SPEC §11: без пересечения с хостом).
|
||
- --no-multicast-peers
|
||
- --no-tcp-relay
|
||
- --denied-peer-ip=10.0.0.0-10.255.255.255
|
||
- --denied-peer-ip=172.16.0.0-172.31.255.255
|
||
- --denied-peer-ip=192.168.0.0-192.168.255.255
|
||
- --no-tlsv1
|
||
- --no-tlsv1_1
|
||
volumes:
|
||
# LE-сертификат TURN-хоста (turns:), скопированный под uid coturn (nobody)
|
||
# renewal deploy-hook'ом. См. docs по развёртыванию TURN-over-TLS.
|
||
- ${CUSTOCRM_INSTANCE_DIR:-.}/data/coturn-certs:/etc/coturn/certs:ro
|
||
healthcheck:
|
||
# Allocation smoke: STUN binding к собственному listener на выделенном IP.
|
||
test: ["CMD", "turnutils_stunclient", "-p", "${CUS_TURN_LISTENING_PORT:-3478}", "${CUS_TURN_LISTENING_IP}"]
|
||
interval: 30s
|
||
timeout: 5s
|
||
retries: 5
|
||
start_period: 15s
|