56 KiB
arch-03. Docker Compose blueprint
Термины (имена бакетов S3, идентификаторы) — в
arch-00-glossary.md. Контракт Message Safety Service — вarch-02-api-contracts.md, раздел «api-backend ↔ message-safety». Переменные окружения и настройки — вarch-04-settings-and-content.md. VM/SSH, OS-роли, секреты, systemd-деплой и hardening — вarch-06-service-hosting-security.md. Детальный контракт nginx (TLS/ACME, request id, internal 404, reload) —arch-08-nginx.md; routing matrix VM —module-03-nginx-vm1.mdиmodule-03-nginx-vm2.md.
Назначение
Этот документ описывает целевой Docker Compose контур для первой production-like среды. Он не заменяет будущий docker-compose.yml, но задает требования, которым он должен соответствовать.
Требования к безопасности на уровне приложения и данных — в arch-01-system-architecture.md, раздел «Принципы безопасности». Настоящий документ описывает только инфраструктурную реализацию этих принципов в compose/nginx: TLS, маршрутизация, сетевые границы, rate limits на edge. Значения переменных окружения — в arch-04-settings-and-content.md. Дублировать прикладные требования (JWT, валидация, CORS в API, PII в логах и т.п.) здесь не нужно — они остаются в arch-01.
Один root Compose project на каждую VM (обязательно)
Это зафиксированное архитектурное требование, а не рекомендация.
Принцип независимого входа по VM
- ВМ1 и ВМ2 имеют по одному независимому root Compose project:
backend/docker-compose.ymlиprocessing/docker-compose.yml. - Каждый project поднимается своим root-owned systemd-unit/deployment helper. Пользователь
deployзапускает только конкретные units и не получает доступ к Docker daemon; канон — arch-06. - Внутри одной VM её корневой
docker-compose.yml— единственный источник правды. Cross-host Docker network и запуск одного Compose project на двух VM запрещены. - Nginx ВМ1 является публичной точкой входа только своего контура:
/api/*→api-backend(REST иWS /api/v1/realtime; отдельный path/realtime/*не используется);/auth/*→keycloak;/bitrix/*(public:/bitrix/handler,/bitrix/install,/bitrix/placement,/health/*дляbitrix-local-app) →bitrix-local-app;- exact
POST /callbacks/idgtl/sms→sms-service; остальные методы и SMS paths не публикуются; - web-сборка frontend или прокси на dev-сервер;
/internal/openlines/*,/internal/safety/*,/internal/sync/*,/internal/sms/*,/internal/notifications/*не публикуются наружу.
- Nginx ВМ2 независимо терминирует public HTTPS на отдельном host и публикует только exact Contact/alert webhook
bitrix-sync. Отдельный private listener8443по internal CA маршрутизирует allow-listed Message Safety/internal paths. - Между public route ВМ1 и ВМ2 нет reverse-proxy chain или fallback. Каждый nginx имеет собственные DNS, сертификат, rate limits и release lifecycle.
После Safety cutover root Compose ВМ1 не содержит local message-safety,
его Redis DB2 или bitrix-sync: это компоненты ВМ2. api-backend использует
MESSAGE_SAFETY_URL=https://<private-vm2-name>:8443 и отдельный read-only CA
bind из root-owned staging-каталога, доступного фактическому UID/GID
api-backend. Validator обязан принимать private HTTPS hostname/SAN и
отклонять cross-host Docker DNS либо plaintext production URL. Наличие legacy
local Safety одновременно с remote URL является cutover error, а не
автоматическим fallback.
Структура Compose через include
Каждый сервис описывается в собственном docker-compose.yml и подключается в root-файл своей VM директивой include:
backend/
docker-compose.yml # root ВМ1
.env
nginx/
docker-compose.yml # описание сервиса nginx (или секция в корневом)
nginx.conf
conf.d/
certs/
.gitkeep
api-backend/
docker-compose.yml # описание сервиса api-backend
bitrix-local-app/
docker-compose.yml # описание сервиса bitrix-local-app
keycloak/
docker-compose.yml # описание сервиса keycloak (или секция в корневом)
observability/
docker-compose.yml # otel-collector и т.п.
processing/
docker-compose.yml # root ВМ2
nginx-internal/docker-compose.yml
message-safety/docker-compose.yml
bitrix-sync/docker-compose.yml
redis/docker-compose.yml
observability/docker-compose.yml
Корневой backend/docker-compose.yml (принципиальная схема):
name: han-chat
include:
- nginx/docker-compose.yml
- api-backend/docker-compose.yml
- bitrix-local-app/docker-compose.yml
- keycloak/docker-compose.yml
- sms-service/docker-compose.yml
- redis/docker-compose.yml
- observability/docker-compose.yml
networks:
public:
backend:
egress:
observability:
volumes:
redis-data:
Root Compose ВМ2 включает собственный nginx с public/private server blocks, Message Safety API/worker, bitrix-sync, Redis Safety и локальный OTEL Collector. clamd/freshclam удалены из Compose; KESL 12.4 standalone и root-owned fail-closed broker работают на host VM2. Секреты, сети и volumes двух projects не общие.
Правила для сервисных compose-файлов
- Сервисный
docker-compose.ymlописывает только сервис(ы) своего модуля: образ, build context, non-secretenvironment(через${VAR}из корневого.env), secret files/credentials, порты (только внутренние, кроме случаев ниже),depends_on, healthcheck, подключение к сетямpublic/backend/observability(объявленным в корневом файле). - Сервисный файл не объявляет сети и volumes верхнего уровня — они объявляются в корневом
docker-compose.yml. Сервис только ссылается на них черезnetworks:/volumes:(external-стиль не нужен, т.к.includeобъединяет файлы в один проект). - Публикация портов наружу (
ports:) разрешена только дляnginx(80/443). Все остальные сервисы используютexpose:для внутренних портов и общаются через Docker-сети. bitrix-local-appне публикует8080на хост (даже на127.0.0.1) — он доступенapi-backendиnginxчерез сетьbackend/public. Ранее применявшийся127.0.0.1:8080:8080считаем устаревшим; проверки через curl на127.0.0.1:8080заменяются наdocker compose exec bitrix-local-appили прокси черезnginx.- Каждый сервисный compose-файл должен запускаться и в составе корневого контура, и автономно (
docker compose -f bitrix-local-app/docker-compose.yml up) для локальной разработки сервиса — при условии, что переменные окружения заданы. Для автономного запуска сервис может объявлять заглушки сетей/volumes, но в составе корневого контура они переопределяются общими. - При сборке образов нужно добавлять защиту на CRLF → LF
Обязательный container hardening
Для production-сервисов применяются требования arch-06-service-hosting-security.md:
- непривилегированный
user; security_opt: [no-new-privileges:true];cap_drop: [ALL]с точечным возвратом документированных capabilities;read_only: true, а writable paths — отдельные volume/tmpfs;- запрет
privileged, host network/PID/IPC и Docker socket; - CPU/memory/PID limits, healthcheck и pinned image version/digest;
- только необходимые Docker networks и read-only bind mounts.
Если сервису нужен root, writable root filesystem, capability или host mount, исключение фиксируется в профильной спецификации вместе с риском и компенсирующей мерой.
Практическая валидация non-root/read-only image
Для каждого pinned digest Compose фиксирует и проверяет:
- фактические UID/GID основного процесса и entrypoint;
- vendor entrypoint для non-root режима, если он отличается от root-варианта;
- полный список writable paths: generated config, runtime/socket, cache/temp, logs и persistent state;
- отдельный volume/tmpfs для каждого writable path с явными
uid/gid/mode, размером и mount flags; - bind file проверяется реальным read-test от container UID/GID и отказом
постороннему UID, а не только host
stat; - новый named volume для non-root consumer подготавливается idempotent
one-shot init service с минимальными capabilities; consumer использует
depends_on: condition: service_completed_successfully; - healthcheck именно того процесса, который реально запущен в контейнере, и только binaries, наличие которых доказано внутри exact pinned digest;
- restart semantics: успешный one-shot exit не должен превращаться в бесконечный restart/download loop.
Tmpfs скрывает ownership каталога из image, поэтому одного корректного
USER/chown в Dockerfile недостаточно: ownership задаётся на самом tmpfs.
Ошибка read-only file system исправляется добавлением минимального writable
mount, а не read_only: false, root, privileged или broad capabilities.
Compose file secrets с bind-backed file: могут игнорировать декларативные
uid/gid/mode. Их фактические host permissions создаёт secret materializer;
rollout проверяет owner/mode из контейнера и с host, не полагаясь на YAML.
root:root 0600 несовместим с non-root consumer; dedicated group/ACL должен
совпадать с фактическим container GID и не давать доступ постороннему UID.
При сборке images необхоидмо добавлять нормализацию CRLF→LF
(например, RUN sed -i 's/\r$//' <directory/service-name>
&& /bin/sh -n <directory/service-name>) и использовать проверку синтаксиса entrypoint
Команды разработки
docker compose up -d
docker compose logs -f nginx
docker compose logs -f api-backend
docker compose logs -f message-safety
docker compose logs -f bitrix-sync
docker compose logs -f bitrix-local-app
docker compose exec api-backend alembic upgrade head
docker compose exec api-backend pytest
docker compose exec api-backend ruff check .
docker compose exec api-backend ruff format .
Сервисы
nginx ВМ1 и ВМ2
На каждой VM работает собственный nginx в независимом root Compose. ВМ1 обслуживает frontend/API/auth/Open Lines/SMS; ВМ2 напрямую принимает CRM webhook и отдельно предоставляет private Message Safety ingress. Публичный трафик ВМ2 не проксируется через ВМ1.
Требования:
- публикует в internet только
80и443(см. политику HTTP ниже); private8443ВМ2 публикуется только в VPC/SG для ВМ1 и ops; - принимает внешний HTTPS-трафик;
- выполняет TLS termination на reverse proxy; внутренний HTTP между контейнерами — только в закрытой Docker-сети
backend; - non-root nginx получает writable tmpfs только для
/etc/nginx/conf.d,/var/cache/nginx,/var/runи/tmp; tmpfs задаёт явные UID/GID/mode иnofileсогласован сworker_connections; - если image entrypoint выполняет
envsubst, output directory обязан быть writable целевому UID, а основнойnginx.confподключает конкретный generated file, чтобы отсутствующий результат не дал ложный успешныйnginx -tчерез wildcard include; - pre-start config test выполняет реальный image entrypoint. До запуска upstream-контейнеров их host variables временно подменяются loopback IP только в test container; production Compose сохраняет service DNS names;
- после readiness upstream выполняется повторный
nginx -tс production Docker DNS names и контролируемый reload/recreate nginx. Первый ordered start иdepends_on: service_startedне гарантируют, что hostname уже резолвится; - при recreate/смене IP upstream действует та же post-ready reload policy либо используется явно протестированный dynamic resolver; stale IP/DNS в загруженной nginx config недопустим;
- применяет каноническую HTTP/HTTPS policy из
arch-08-nginx.md: web/единый MVP host использует только308на HTTPS, выделенный API host не слушает:80; единственное route-specific исключение — HTTP exact CRM webhook ВМ2 возвращает404/426без redirect и без отражения query token; - маршрутизирует
/api/*вapi-backend(включая WebSocket upgrade для/api/v1/realtime); - маршрутизирует
/auth/*вkeycloakили проксирует отдельный auth-домен; - маршрутизирует публичные
/bitrix/*endpoint вbitrix-local-app; - nginx ВМ2 маршрутизирует только exact
/bitrix/sync/webhook/contactи/bitrix/sync/webhook/alertв локальныйbitrix-sync; - маршрутизирует только exact
POST /callbacks/idgtl/smsвsms-service:8080; применяет HTTPS, подтверждённый allowlist source IP Direct, body/rate limits и redaction Basic Authorization; - закрывает
/internal/*(в т.ч.bitrix-local-app,message-safety,bitrix-syncops) от публичного доступа — только private network Docker/VPC; - не публикует
message-safetyнаружу; - production-like / production: отдаёт статическую сборку Expo web из volume или каталога (
/usr/share/nginx/htmlили аналог);index.html+ assets, SPA fallbacktry_files $uri /index.html; - local dev (опционально): при
FRONTEND_DEV_PROXY_ENABLED=trueпроксирует/на Expo dev server (EXPO_DEV_SERVER_URL, напр.http://host.docker.internal:8081); - передает upstream-сервисам
Host,X-Real-IP,X-Forwarded-For,X-Forwarded-Proto,X-Forwarded-Host,X-Request-ID; - если входящий запрос без
X-Request-ID, nginx генерирует UUID и устанавливает заголовок до proxy_pass (I3); - задает разумные
proxy_connect_timeout,proxy_read_timeout,client_max_body_size; - для
POST /api/v1/dialogs/*/messagesproxy_read_timeoutдолжен быть не меньшеMESSAGE_SAFETY_TASK_POLL_MAX_SEC + 30s, чтобы nginx не обрывал sync-wait при async file scan; - применяет edge rate limits для auth, API и download endpoints;
- ограничивает частоту соединений и размер тела запроса;
- разрешает только TLS 1.2/1.3 и запрещает слабые шифры;
- добавляет HSTS и базовые security headers;
- скрывает заголовки, раскрывающие внутренние технологии;
- кэширует публичные endpoint настроек и контента;
- не проксирует наружу managed PostgreSQL,
redis,otel-collector(БД вне compose, в VPC);
api-backend
Python FastAPI backend.
Требования:
- запускается после доступности managed PostgreSQL,
keycloak,redis; - применяет non-secret настройки из
.envи runtime secrets из явно смонтированных secret files; - отдает
/health/liveи/health/ready; - корректно работает за reverse proxy и доверяет proxy headers только от
nginx; - применяет API-level rate limits с состоянием в Redis;
- вызывает message safety pipeline для сообщений до отправки в Open Lines;
- вызывает
bitrix-local-appдля отправки сообщений в Open Lines; - принимает forward нормализованных событий оператора от
bitrix-local-app; - поддерживает realtime endpoint для сообщений оператора;
- работает с Selectel S3 для файлов и документов;
- обслуживает Notification Center G/P и Internal Create/Cancel; token каждого продюсера передаётся через env/secret, в App DB хранится только hash;
- имеет отдельные процессы expire job (ежедневно 00:01 UTC, advisory lock) и cleanup upload drafts/S3; они используют тот же immutable image и не публикуют порты;
- экспортирует traces/logs в
otel-collector; - не хранит состояние внутри контейнера.
message-safety
Отдельный сервис ВМ2 для проверки исходящих сообщений пользователя. HTTP-контракт — в arch-02-api-contracts.md, раздел «api-backend ↔ message-safety».
Требования:
- запускается после доступности managed PostgreSQL (схема
message_safety) и Redis Safety; - сам Message Safety наружу не публикуется; api-backend обращается через private listener nginx ВМ2 по HTTPS;
- отдаёт
/health/liveи capability-aware/health/ready: PostgreSQL/config — core gate, Redis/workers/S3/DNS влияют на отдельные capabilities; в MOCK normal capabilities показываются какbypassed, mode —degraded; - target endpoints:
POST /internal/safety/v2/messages/check,GET /internal/safety/v2/messages/tasks/{task_id}(private HTTPS +X-Service-Token); - read-only доступ к S3-quarantine (отдельный access key без прав записи);
- использует отдельную схему
message_safetyв managed PostgreSQL и отдельный DB-user; - runtime role читает immutable active
message_safety.config_versions; создавать/активировать config может только отдельный migration/config-admin job; - использует локальный Redis Safety только для hot cache/rate/wakeup; PostgreSQL владеет task queue/leases;
- запускает async workers для file scan из S3-quarantine;
- worker монтирует только Unix socket
/run/han-kesl/scan.sockroot-owned broker; KESL иkesl-controlостаются на host, TCP scanner port отсутствует; - API container получает read-only
/etc/han-chat/message-safety-mode.envс host contractroot:han-message-safety 0640и GID10001; менять его и перезапускать stack может только fixed helper, разрешённыйdeployчерез exact-argument sudoers; - экспортирует traces/logs в
otel-collector; - таймауты: POST check 5 s, GET task 2 s, file scan 60 s (см.
arch-04-settings-and-content.md, переменныеMESSAGE_SAFETY_*).
Host KESL и broker на ВМ2
KESL 12.4 работает standalone на host и не является Compose service. Отдельный
root-owned custom broker слушает только /run/han-kesl/scan.sock и вызывает
фиксированный kesl-control --scan-file --action Inform.
- socket монтируется только в Message Safety worker и доступен выделенной группе; worker не получает host binary, shell, Docker socket или host root;
- broker принимает только bounded scan request и fail-closed сопоставляет
clean → allow,infected → deny, а timeout, stale database, неизвестный output/exit code и недоступность KESL → retry/503; scanner_engine=kesl;signatures_version— hash KESL version + database date;- KESL обновляет database ежечасно на host; update egress не подключает контейнеры к internet;
- schema допускает
max_signature_age_hoursдо720, seed —240.
Формат результата kesl-control, socket permissions, cleanup и throughput
этой custom integration подтверждаются gates на target VM2 с фактическим KESL
12.4; repository-only/Compose health не считается достаточным evidence.
bitrix-sync
Python API/worker service ВМ2 для durable двусторонней синхронизации App DB ↔ Bitrix24 CRM. Каноническая постановка — module-07-bitrix-sync.md.
Требования:
- запускается при доступном managed PostgreSQL; Redis не является зависимостью sync;
- читает задачи из
han_app.sync_queue(заполняется триггерами App DB); - владеет схемой
bitrix_syncи имеет только точечные GRANT на queue/profile/mapping вhan_app; - выполняет durable workflows
contact.map_or_create,contact.update,contact.deactivateи административныйcontact.rebind; - принимает
POST /bitrix/sync/webhook/contactи/bitrix/sync/webhook/alert, durable сохраняет до2xx; - выполняет Contact/alert reconciliation на случай потери обычного webhook;
- при записи в App DB от Bitrix использует
SET LOCAL han.sync_suppress='true'; - использует Bitrix
batch, общий token bucket и bounded in-flight; default 2 HTTP requests/sec; - поддерживает leases/fencing, graceful shutdown, retry до 24 часов, technical DLQ и business alerts;
- не блокирует пользовательский API при ошибках Битрикс24;
- не участвует в OTP-flow, не создаёт
UserIdentity/ClientProfile; - не участвует в hot path чата Open Lines;
- принимает публичный CRM webhook после TLS termination/rate limit на собственном nginx ВМ2; ВМ1 в route не участвует;
- включается/отключается флагом
BITRIX_SYNC_ENABLEDв.env(defaulttrue): приfalseсервис стартует в no-op/degraded режиме, но не обрабатываетsync_queueи не выполняет синхронизацию с Bitrix24 CRM.
bitrix-local-app
Локальное приложение Bitrix24 и custom connector han_mobile_app.
Требования:
- публикует наружу только
/bitrix/handler,/bitrix/install,/bitrix/placement,/health/live,/health/ready; - принимает
ONAPPINSTALLиONIMCONNECTOR*события от Bitrix24; - регистрирует и активирует connector
han_mobile_appдля открытой линии 8; - хранит OAuth-токены Bitrix24, inbox событий и
dialog_sessionsв managed PostgreSQL, схемаbitrix_local; - предоставляет internal API
POST /internal/openlines/v1/messagesиGET /internal/openlines/v1/dialogs/{external_chat_id}для api-backend; - защищает internal API через
Authorization: Bearer {BITRIX_INTERNAL_API_TOKEN}; - forward-ит нормализованные события Open Lines в API, если задан
BITRIX_API_FORWARD_URL; - не хранит бизнес-данные приложения и не пишет напрямую в App DB.
Managed PostgreSQL
Во всех средах (production, production-like, local dev) данные хранятся в managed PostgreSQL провайдера. Контейнер PostgreSQL в Docker Compose не используется — ни для production, ни для локальной разработки.
Прикладные данные, Keycloak, bitrix-sync, bitrix-local-app и message-safety подключаются к одной managed базе по URL из .env (HAN_PG_HOST, HAN_PG_PORT, HAN_PG_DATABASE и схемо-специфичные *_DATABASE_URL).
Требования:
- подключение только из приватной сети VPC (VM → managed PostgreSQL);
- одна managed база: схемы
han_app,bitrix_sync,message_safety,bitrix_local,keycloak,sms; - отдельные DB-пользователи с доступом только к своей схеме; исключение:
bitrix_sync_userдополнительно имеет column/table GRANT наhan_app.sync_queue, чтение необходимых identity/profile columns и controlled update CRM-master profile fields согласно module-07 §13; mapping/rebind находятся в собственной schemabitrix_sync; - TLS к managed PostgreSQL обязателен;
- миграции Alembic выполняются отдельным controlled job с migration URL, который не попадает в runtime services;
- временные cross-schema
USAGE/SELECTвыдаёт owner/DB administrator на конкретные объекты до migration gate и отзывает после успешного commit; migration чужой схемы не выполняетREVOKE/ALTERеё объектов; - release image сохраняет все уже использованные Alembic revision IDs, в том
числе legacy/no-op baseline, чтобы
upgrade headне требовал ручногоstampproduction DB; - бэкапы и PITR — на стороне провайдера.
keycloak
Identity provider. Обязателен в compose-контуре с первого запуска.
Требования:
- отдельный realm для приложения;
- отдельный frontend client с PKCE (обязателен);
- confidential backend client — optional (не используется в hot path MVP; S2S между сервисами — service tokens);
- публичный issuer должен соответствовать HTTPS URL, видимому frontend-приложению;
- включены proxy settings для работы за
nginx; - импорт realm в local/dev;
- использует managed PostgreSQL, схема
keycloak(см. раздел «Managed PostgreSQL» выше); - OTP mock / SMS SPI — см. arch-04; real mode вызывает
sms-serviceпо сетиbackend; приKEYCLOAK_YANDEX_CAPTCHA_ENABLED=trueKeycloak подключается кegressс destination allow-list только для server-side validation Yandex SmartCaptcha, приfalseegress у Keycloak отсутствует; - healthcheck;
- взаимодействия —
arch-02-api-contracts.md, «Frontend ↔ Keycloak», иarch-01-system-architecture.md, «Keycloak».
sms-service и sms-worker
sms-service: networksbackend,observabilityиegressтолько если тот же process принимает callback и выполняет worker;expose: 8080, без hostports.- При отдельном
sms-worker: networks толькоegress,observabilityи доступ к managed PG; HTTP port не exposed/published. - Оба используют
SMS_DATABASE_URLк schemasms; только worker получаетIDGTL_SMS_API_KEY. - Callback credentials получает receiver для проверки и worker для формирования callback URL; Keycloak получает только
KEYCLOAK_SMS_SERVICE_TOKEN. sms-serviceприменяет собственные versioned migrations/seed; DDL-on-start запрещён. Readiness проверяет DB/schema, active approvedauth_otptemplate, sender и API-key configuration.- Ожидание Direct до 70 секунд происходит только в worker.
uncertainне retry-ится автоматически; provider outage не создаёт restart loop и не отменяет active Keycloak challenge.
redis
Кэш, rate limiting, coordination (не единственное хранилище бизнес-событий).
Требования:
- не использовать как единственное надежное хранилище бизнес-событий;
- хранить счетчики API-level rate limits и idempotency keys (
api-backend); - поддерживать TTL для лимитных и idempotency ключей;
- не хранить OTP counters для
api-backend(OTP — зона Keycloak/SPI); - sync_queue, leases, limiter coordination и durable wake-up fallback хранятся в PostgreSQL;
LISTEN/NOTIFY— только optimization, Redis sync-service не использует; - разделение DB index (I4): см. arch-04 (
REDIS_URL,MESSAGE_SAFETY_REDIS_URL); - детальный контракт и карта ключей —
arch-09-redis.md,module-04-redis-vm1.md,module-04-redis-vm2.md.
otel-collector
Принимает telemetry от сервисов.
Требования:
- OTLP HTTP/gRPC receiver;
- экспорт traces/logs в stdout или платформенный collector;
- единые resource attributes:
service.name,deployment.environment.
Networks
Рекомендуемые сети:
- ВМ1
public: edge nginx, Keycloak proxy и frontend entrypoint. - ВМ1
backend:api-backend,bitrix-local-app, Keycloak, SMS API и Redis DB0/DB1. - ВМ2
backend: nginx, Safety API/worker,bitrix-syncи Redis Safety; broker доступен worker только через host Unix socket. - ВМ1
egressподключается только к процессам с назначением:sms-worker→ i-Digital Direct; Keycloak → SmartCaptcha только при включённом feature flag;api-backend→ S3 и private PG, а вызов Safety идёт кprocessing.internal:8443; local collector → private SigNoz.sms-serviceбез совмещённого worker,bitrix-local-appи Redis не получают общий internet egress. - ВМ2
egressподключается только к процессам с назначением:bitrix-sync→ утверждённый Bitrix portal; Safety worker → S3/PG/DNS; collector → private SigNoz. Общего internet egress у Safety API/Redis нет; KESL update egress задаётся отдельно на host. observabilityсуществует отдельно на каждой VM и ведёт в её local collector.
Базы данных, Redis, OTLP receivers и internal service ports не публикуются. Cross-host calls идут через private network, точные SG и TLS.
Volumes
Минимальные persistent volumes:
- ВМ1: Redis DB0/DB1 data, local OTEL queue;
- ВМ2: Redis Safety data (rebuildable), internal TLS secrets, local OTEL queue.
KESL database/runtime и
/run/han-keslпринадлежат host, не Compose volumes.
Public TLS и ACME на обеих VM — не named volumes. Root-only ACME state остаётся
на host в /etc/letsencrypt; root hook атомарно копирует только нужные
fullchain.pem/privkey.pem в /var/lib/han-chat/public-tls. Compose
монтирует этот staging read-only в nginx, а host webroot
/var/lib/han-chat/acme — в nginx и ACME client с минимально необходимыми
правами. Internal TLS certificate/key передаются отдельными Compose secrets и
не объединяются с public TLS.
Данные PostgreSQL не хранятся в Docker volumes — только managed PostgreSQL вне compose.
Persistent volume используется только для state, переживающего recreate. Generated config, PID/socket, cache и logs без retention размещаются в ограниченных tmpfs. Один writable volume не объединяет config/executable со state.
Local OTEL queue на каждой VM использует отдельный persistent named volume.
Перед collector запускается otel-queue-init: ограниченный one-shot service
выставляет mount root в 10001:10001 0700, не имеет сети/secrets и завершается
до старта collector. Collector остаётся non-root; запуск collector от root,
chmod 0777 и надежда на автоматическое наследование ownership из image
запрещены.
Переменные окружения
Каждая VM имеет свой allow-listed non-secret env manifest. Секреты доставляются отдельными service files согласно arch-04/06; общий env/secret bundle двух VM запрещён.
Для smoke-продюсера обязателен NOTIFICATIONS_TOKEN_PRODUCER_TEST; это runtime secret, а не .env/app_settings. Compose монтирует его только api-backend и notification workers. Зарегистрированные entrypoints: han-notification-expire-worker и han-notification-draft-cleanup-worker; выдуманный command без project script в deployment запрещён.
HTTPS и TLS
Соответствует arch-01-system-architecture.md, «Принципы безопасности» (HTTPS, TLS, HSTS). Инфраструктурная реализация:
Домены и HTTP
| Host | Порт 80 | Порт 443 | Примечание |
|---|---|---|---|
| Веб-домен (frontend) | только 308 → HTTPS |
HTTPS, бизнес-логика | MVP: tohin.ru; staging/dev может использовать отдельный host |
| API-домен (если выделен) | не слушает | только HTTPS | Post-MVP: api.example.ru |
| Bitrix Local App ВМ1 (`/bitrix/handler | install | placement`) | только 308 на HTTPS |
| CRM webhook ВМ2 (exact `/bitrix/sync/webhook/contact | alert`) | generic 404/426, без redirect query token |
HTTPS |
Правила:
- все внешние пользовательские соединения — HTTPS;
- HTTP допускается только на веб-домене как вход для редиректа на HTTPS;
- для выделенного API-домена HTTP не допускается (нет listener на :80);
- при едином домене MVP redirect на :80 применяется ко всему host, включая
/api/*и/auth/*, после редиректа — только HTTPS.
TLS и заголовки
TLS versions, trusted CA, HSTS/security headers, certificate validation и safe
reload принадлежат arch-08-nginx.md; cookie/OIDC и
application security — arch-01-system-architecture.md.
Compose применяет их через host binds из раздела «Volumes», не монтирует
root-only /etc/letsencrypt в nginx, не объявляет named volume public
certificates и не публикует внутренние порты.
Nginx routing для Bitrix24 Local App
nginx должен поддерживать отдельные server/location rules для bitrix-local-app.
Рекомендуемая схема:
- веб-домен (MVP:
tohin.ru):/api/*(REST + WS realtime),/auth/*, web frontend;:80→308HTTPS;:443— TLS + маршрутизация; - выделенный API-домен (post-MVP, опционально): отдельный
server { listen 443 ssl; ... }безlisten 80; только/api/*; - для
locationWebSocket (/api/v1/realtime):proxy_http_version 1.1,Upgrade/Connectionheaders, увеличенныйproxy_read_timeout; - только
/bitrix/handler,/bitrix/install,/bitrix/placementна ВМ1 →bitrix-local-app; CRM/bitrix/sync/*на этом host не маршрутизируется; GET/POST /bitrix/handlerиGET/POST /bitrix/installдоступны публично для Bitrix24;/bitrix/placementдоступен публично как заглушка UI настроек коннектора;/health/liveи/health/readyдляbitrix-local-appдоступны только там, где это нужно для healthcheck и проверки Bitrix form URL;/internal/openlines/v1/*не публикуется наружу или защищается allowlist/private network плюсAuthorization: Bearer {BITRIX_INTERNAL_API_TOKEN};- для
/bitrix/*callbacks кэширование отключено; - для
/bitrix/*callbacks включены отдельные rate limits, но они не должны блокировать легитимные webhook-повторы Bitrix24.
Nginx routing для bitrix-sync (CRM webhooks)
Публичный nginx ВМ2 маршрутизирует только два exact webhook CRM sync в локальный bitrix-sync:
POST /bitrix/sync/webhook/contact— изменение Contact;POST /bitrix/sync/webhook/alert— изменение элемента smart process конфликтов;- nginx до proxy проверяет source IP по version-controlled
BITRIX_WEBHOOK_ALLOWED_CIDRS; автоматическое расширение allow-list запрещено; - штатный робот передаёт отдельный Contact/alert receiver token в query и form-urlencoded document/auth fields; token, query и body не попадают в logs/traces;
- document/entity/domain/member fields проверяются в
bitrix-sync; local app/event handler для CRM sync не используется; - кэширование отключено; source IP/body/method/rate limits применяются до private proxy; IP rejects экспортируются в telemetry без IP label;
- при sync disabled/cutover route закрыт либо возвращает retryable
503, а не202 ignored; BITRIX_SYNC_ENABLED=falseтребует активный edge allow-list только сdeny all;; наличиеallowпри disabled и отсутствие reviewedallowпри enabled являются preflight error;/internal/sync/v1/*не публикуется наружу (только internal network +BITRIX_SYNC_SERVICE_TOKEN).
Rate limits и защита от abuse
Rate limits должны быть распределены по двум слоям.
nginx:
- ограничивает частоту запросов до попадания в API;
- держит отдельные зоны лимитов для
/auth,/api, public endpoints, fallback polling, download endpoints, чтения/действий/загрузок Notification Center; - ограничивает
client_max_body_size; - ограничивает загрузку файлов лимитом 5 МБ;
client_max_body_sizeдолжен быть чуть выше бизнес-лимита для учета overhead запроса; - применяет
limit_reqдля endpoint авторизации и fallback polling; - для публичных endpoint использует лимит не выше 60 запросов в минуту с одного IP, если настройки не говорят иначе;
- возвращает
429при превышении лимитов; - не должен использоваться для сложных пользовательских правил, завязанных на
user_id.
API:
- применяет лимиты после проверки JWT;
- считает лимиты по
user_id, IP, route, dialog id и service client; - хранит быстрые счетчики в Redis;
- пишет значимые превышения в audit/App DB;
- возвращает
Retry-After, если клиент может повторить запрос позже.
Проверка сообщений на prompt injection и вредоносные действия не должна выполняться в nginx: это задача отдельного сервиса message-safety, вызываемого из api-backend (см. arch-02-api-contracts.md).
WAF
WAF можно подключить внешним слоем перед nginx без изменения бизнес-кода, если соблюдены требования:
nginxи API корректно работают с цепочкой proxy headers и доверяют real IP только от доверенных прокси;- CORS разрешает только доверенные домены;
- публичные endpoint имеют rate limits и кэширование даже без WAF;
- схема TLS termination согласована с тем, где завершается TLS: WAF/CDN, load balancer или
nginx; - WAF не должен подменять тело запросов и ответы API без явной необходимости.
WAF не заменяет обязательные лимиты, валидацию схем, авторизацию и аудит внутри приложения.
Публичные endpoint
GET /api/v1/public/app-config и GET /api/v1/public/content являются публичными, поэтому для них обязательны:
limit_reqна уровнеnginx, базово 60 запросов в минуту с одного IP;- агрессивное кэширование на уровне
nginxили CDN; - заголовок
Cache-Control: public, max-age=3600; - строгая DTO-схема ответа на backend, без сериализации всех строк таблицы настроек;
- CORS только для доверенных доменов приложения;
- отсутствие секретов, внутренних URL, service tokens и приватных feature flags в ответе.
Подробнее — arch-04-settings-and-content.md, разделы «Публичный config endpoint» и «Публичный content endpoint».
Healthchecks
Минимальные проверки:
nginx: на веб-домене —308с:80на HTTPS; на API-домене (если выделен) —:80не слушает;:443— успешная TLS handshake и ожидаемый route response;api-backend:/health/liveпроверяет процесс;/health/readyпроверяет PostgreSQLhan_app, Redis DB0/DB1, JWKS/discovery Keycloak и S3 permissions. Недоступность remote Message Safety отражается как degraded dependency и блокирует только send path, но не readiness read API;message-safety:/health/readyвозвращает process/core status и capability maptext|links|files|worker; KESL broker/stale database/S3 не выключают text, DNS не выключает text без ссылок, Redis hot cache не является core gate;bitrix-sync:/health/liveпроверяет процесс;/health/readyпроверяет validated config/secrets, PostgreSQL/grants, worker/limiter state и CRM webhook config; invalid credential/config даёт not-ready, краткая CRM outage — degraded по stale policy; приBITRIX_SYNC_ENABLED=falseready возвращает not-readysync_disabled;bitrix-local-app:/health/liveпроверяет процесс;/health/readyпоказывает PostgreSQL, OAuth-токены после установки приложения, connector activation и возможность forward в API при включённомBITRIX_API_FORWARD_URL;keycloak: health endpoint Keycloak; readiness — подключение к managed PostgreSQL;sms-service: live — процесс; ready — schema/migrations, active approved template, sender/API key; Direct доступность — отдельный dependency status, не причина restart loop;redis:redis-cli ping;
Наружу через nginx публикуются только health endpoint, которые нужны Bitrix24 install/callback validation или внешнему мониторингу. Internal services (message-safety, internal bitrix-sync, Redis, otel) проверяются только из Docker/VPC-сети.
Healthcheck считается валидным только после запуска внутри exact production
image digest с теми же user, read_only, mounts и dropped capabilities.
Команда не может предполагать наличие curl, wget, shell или package,
которого нет в image/SBOM; ExitCode 127 является дефектом release, а не
unhealthy приложения. Для minimal nginx предпочтителен гарантированно
доступный native binary/config test либо специально включённый и проверенный
client.
Healthcheck не копируется между разными commands одного image без проверки.
Если updater не запускает daemon, daemon-socket healthcheck для него
отключается. Freshness данных контролируется отдельной метрикой/проверкой
timestamp и версии, а restart: unless-stopped применяется только к
долгоживущему foreground process.
Порядок запуска
Общие prerequisites: managed PostgreSQL доступна из private network, host-side secrets/TLS materialized, controlled migrations и seed завершены.
ВМ2 запускается в порядке:
- operator KESL runbook: KESL 12.4/database update, затем enable/start broker socket и проверка status/permissions;
redis-safety,otel-queue-init, затем localotel-collector;- Message Safety API/worker и
bitrix-sync; - nginx — последним, после успешного config test;
- private HTTPS ВМ1→ВМ2 и capability health проверяются до cutover.
ВМ1 запускается в порядке:
- Redis,
otel-queue-init, затемotel-collector; - Keycloak,
sms-service/worker,api-backendиbitrix-local-appс их readiness-зависимостями; - notification expire/cleanup workers после готовности
api-backend; - nginx — последним.
Порядок rollout SMS подробнее задаёт module-11/module-10. Зависимости запуска не образуют цикл: Keycloak стартует при недоступном sms-service; это блокирует только новые real-mode orders, а verify уже active challenges продолжается по snapshot.
depends_on не заменяет проверку готовности. Для edge применяются три слоя:
ordered start, readiness dependencies и post-ready config test/reload с
production DNS names. Сервисы должны уметь ждать зависимости или корректно
деградировать. api-backend может стать ready для read API до ВМ2, но send
endpoint обязан fail-closed при недоступной требуемой capability Safety.
Развёртывание на ВМ1 и ВМ2
- ВМ1, ВМ2, SigNoz и managed PostgreSQL находятся в одной private network/VPC; ВМ1 и ВМ2 имеют независимые public DNS/TLS ingress.
- Managed PostgreSQL не имеет public IP; SG разрешает каждой VM только нужные DB roles/schemas.
- На каждой VM отдельный root-owned systemd unit выполняет её root Compose;
deployне входит вdocker. - Public nginx ВМ2 публикует
80/443;80обслуживает ACME, а HTTP exact CRM webhook возвращает404/426без redirect;443публикует только exact CRM webhook. Private8443разрешён только от SG ВМ1 и ops для Message Safety/internal access. - Deploy/cutover ВМ2 не требует изменения public routes ВМ1. Для
bitrix-syncrollback закрывает webhook routes на nginx ВМ2 либо возвращает retryable503, останавливает claims и сохраняет durable tasks/mapping; возврат к фиктивному202 ignoredзапрещён.
Host firewall обеих VM учитывает post-DNAT semantics DOCKER-USER: policy
published nginx ports сопоставляет original host destination через conntrack,
как определено в arch-06. Проверка только UFW INPUT или текущего container
--dport не является достаточной.
Перед первым up и после смены любого image digest обязательны permission
gates:
- проверить LF/shebang и root ownership release-артефактов;
- проверить manifest modes и executable bit scripts/preflight/hooks; blanket
F644для всего release запрещён; - сверить UID/GID контейнеров с owner/mode host bind mounts, secret files и TLS staging;
- проверить доступ целевого UID и отказ постороннему UID, включая traverse parent directories;
- выполнить/проверить one-shot ownership init для non-root named volumes;
- выполнить PEM parse/key-match и Compose render;
- запустить image-native config test/entrypoint/healthcheck с production hardening и временными test-only upstream values;
- выдать migration-role только необходимые временные cross-schema grants, выполнить Alembic и отозвать grants владельцем;
- после readiness upstream повторить edge config test/reload с production Docker DNS names;
- после старта проверить отсутствие permission/restart loops, реальные healthchecks и freshness updater data.
Неуспех gate исправляется в ownership, mount/entrypoint contract или DB grants.
Временное ослабление read_only, запуск root, broad chmod, добавление в
docker group и расширение DB privileges запрещены.
Production-замечания
Frontend (Expo web)
| Режим | Поведение nginx |
|---|---|
| production-like / production | Статика Expo web (expo export / EAS web build), FRONTEND_DEV_PROXY_ENABLED=false |
| local dev | Опционально proxy на Expo dev server, FRONTEND_DEV_PROXY_ENABLED=true |
Переменные — arch-04-settings-and-content.md, блок «Frontend (nginx)».
Два root Compose projects — production-контур первого этапа. Позже при росте можно отдельно решить:
- вынос Redis в managed cache;
- перенос
bitrix-syncна ВМ3; - горизонтальное масштабирование Safety API/worker/scan lanes;
- managed internal load balancer/mTLS;
- HA ВМ2.
Secret manager не является будущей опцией: для VM с утверждённым egress действует han-secrets + Selectel Secrets Manager, а для private/no-egress VM — контролируемая доставка root-owned secret files без сетевого secret-agent. Модель зафиксирована в arch-04 и arch-06.
Backup, restore и cleanup
- Managed PostgreSQL должен иметь ежедневные backups и PITR; целевые RPO/RTO для MVP фиксируются в ops runbook до production-запуска.
- S3-data (
attachments,documents) хранит production-файлы; удаление выполняется только через lifecycle, retention или явный audit-backed процесс. - S3-quarantine очищается периодическим cleanup job: удаляются просроченные объекты без активного
MessageAttachment/safety_tasksили объекты с завершённым deny/failed lifecycle. - Redis не является единственным хранилищем бизнес-событий; потеря Redis не должна терять сообщения, sync tasks или audit.