Files
chatballs/compose.yaml
T
AndreyandClaude Fable 5.1 0ca9eb865f ✨ feat(setup)!: мастер первого запуска и демо-набор «Ателье Норд» с точным удалением
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>
2026-09-05 16:55:31 +03:00

270 lines
12 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Канонический 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