Deployment — Payment Manager¶
Гайд по развёртыванию Payment Manager: Docker-образ, env-конфигурация, внешние зависимости (Postgres / Redis / TigerBeetle), порядок запуска миграций, health-checks и worker roles. Документ описывает текущее состояние артефактов в репозитории — Dockerfile, docker-compose.yml и точку входа src/server.ts.
PM — stateless Node.js-сервис (Fastify + TypeScript), один процесс совмещает API и фоновых воркеров через переменную WORKER_ROLES. Никаких локальных файлов состояния: всё хранится в PostgreSQL (pm.*), TigerBeetle и Redis.
1. Docker-сборка¶
PM собирается как multi-stage образ на базе node:22-bookworm-slim (projects/payment-manager/Dockerfile).
FROM node:22-bookworm-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm i
COPY tsconfig.json ./
COPY src/ ./src/
RUN npx tsc
FROM node:22-bookworm-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm i --omit=dev
COPY --from=builder /app/dist ./dist
COPY drizzle/ ./drizzle/
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
Ключевое:
- Builder stage — устанавливает все зависимости (включая dev — нужен
typescript), компилирует TS →dist/. - Runtime stage — переустанавливает только prod-зависимости (
--omit=dev), копирует артефактdist/и каталогdrizzle/(нужен для миграций — Drizzle читает SQL-файлы из него в рантайме). USER node— контейнер запускается под непривилегированным пользователем.EXPOSE 3000— HTTP-порт по умолчанию (управляетсяPORT).NODE_ENV=productionвыставляется в runtime-стейдже — это влияет на Drizzle SQL-логгер и формат ошибок Fastify.
Сборка вручную:
2. Переменные окружения¶
PM конфигурируется исключительно через env. Все ключи валидируются Zod-схемой в src/shared/config.ts при старте — невалидное окружение завершает процесс ещё до app.listen().
Полный реестр переменных, типы, дефолты и требования — в ../reference/env-vars.md.
Обязательные на старте: DATABASE_URL, REDIS_URL, TB_ADDRESS, ADMIN_SECRET, SERVICE_SECRETS.
Опциональные с разумными дефолтами: PORT, WORKER_ROLES, PSP_NAMES, интервалы воркеров.
Шаблон .env.example лежит рядом с Dockerfile.
3. Внешние зависимости¶
PM не запускается, если недоступна любая из трёх систем.
3.1 PostgreSQL (схема pm.*)¶
- Используется как primary store для intents, outbox, PSP-маппинга, fee-rules.
- Подключение — через
postgres(npm-пакет), пул конфигурируется вsrc/shared/db.ts. - В docker-compose рекомендуется ходить через PgBouncer (
pool_mode=transaction) — это покрывает короткие транзакции PM и снижает нагрузку на Postgres при множестве воркеров. - PM работает с
search_path=pm,public: пишет только вpm.*, читает таблицы Auth Center изpublic.*(например,public.profileдля merchant-проверок).
3.2 Redis¶
- Pub/Sub (обязательно) — публикация статусов intent (
intent.{id}), которые Auth Center стримит во Flutter. - Streams (опционально, Phase 2B) — для внешних PSP-адаптеров. В Phase 1 PSP-воркер использует Postgres-очередь
psp_tx_mapи Redis Streams не требуется. - Параметр подключения —
REDIS_URL(redis://[:password@]host:port[/db]). - Для отдельного Redis под
stream.notifications.jobsестьNOTIFICATIONS_REDIS_URL(по умолчанию равенREDIS_URL).
3.3 TigerBeetle¶
- Адрес реплики —
TB_ADDRESS(например,127.0.0.1:3001илиtigerbeetle:3000в docker-сети). - PM — единственный сервис с write-доступом к TB. Подключение устанавливается на старте:
connectTb(config.TB_ADDRESS)с ретраями (5попыток с экспоненциальной паузой). Если за все попытки TB недоступен — процесс аварийно завершается (exit 1). - На старте PM также проверяет/создаёт системные TB-аккаунты (
seedAccounts+verifySystemAccounts) — это идемпотентно, безопасно при повторных запусках.
4. Миграции перед стартом¶
Миграции всегда запускаются перед первым node dist/server.js (и при каждом деплое — Drizzle сам разберётся, что уже применено).
# в проде — после установки prod-зависимостей и копирования drizzle/
npx drizzle-kit migrate
# либо альтернативная точка входа (если в образе нужен только runtime-CLI)
node dist/migrate.js
Скрипты из package.json:
| Скрипт | Команда | Когда использовать |
|---|---|---|
npm run db:migrate |
drizzle-kit migrate |
Применить накопленные миграции к указанной БД |
npm run migrate |
node dist/migrate.js |
То же самое, но через скомпилированный CLI (удобно в Docker без drizzle-kit в prod-зависимостях) |
npm run db:seed |
tsx drizzle/seed.ts |
Засеять pm.service_key, pm.fee_rules, pm.payment_routes |
Подробности (порядок миграций, идемпотентность, seed-набор) — в ./seed-and-migrations.md.
Важно: PM никогда не пишет SQL-миграции вручную и никогда не правит
public.*(это зона Serverpod / Auth Center).
5. Worker roles¶
src/server.ts стартует один процесс. Поведение определяется WORKER_ROLES — comma-separated список:
| Роль | Что запускает |
|---|---|
api |
HTTP-сервер на config.PORT. Без неё процесс работает в headless-режиме. |
outbox-worker |
OutboxWorker — рассылка событий из pm.outbox_event (single-instance, см. ниже). |
psp-worker |
PspWorker для каждого имени из PSP_NAMES (+ BalanceMonitor, если BALANCE_MONITOR_ENABLED=true). |
invoice-expiry |
Sweep-задача, истекающая просроченные MERCHANT_INVOICE intent-ы. |
Полное описание — в ./worker-roles.md.
Типовые комбинации:
# монолит «всё-в-одном» (dev, маленький прод)
WORKER_ROLES=api,outbox-worker,psp-worker PSP_NAMES=IPPS node dist/server.js
# API-только (за nginx, горизонтально масштабируется)
WORKER_ROLES=api node dist/server.js
# headless outbox (ровно одна реплика)
WORKER_ROLES=outbox-worker node dist/server.js
# headless psp-worker (multi-instance safe)
WORKER_ROLES=psp-worker PSP_NAMES=IPPS node dist/server.js
# отдельный invoice-expiry sweep
WORKER_ROLES=invoice-expiry node dist/server.js
Правила масштабирования:
api— горизонтально, сколько угодно реплик.outbox-worker— ровно одна реплика (см. NO-GO вCLAUDE.md: одновременный запуск нескольких воркеров приведёт к дублирующим публикациям).psp-worker— multi-instance safe (CTE +FOR UPDATE SKIP LOCKEDгарантирует, что однаpsp_tx_mapстрока достанется только одному воркеру).invoice-expiry— рекомендуется одна реплика; sweep идемпотентен, но множественные параллельные проходы лишь нагружают БД.
6. Health-чек¶
PM публикует единственный health endpoint:
200 { status: "ok", version, timestamp }— event loop здоров.503 { status: "ko", message: "Service under pressure" }—@fastify/under-pressureзафиксировал event-loop delay >1000 ms.
Health-роут не открывает новых соединений к БД / TB / Redis: исправность инфраструктуры проверяется один раз на старте, и при сбое процесс падает до app.listen(). Подробности (что именно валидируется, какие коды возврата, как использовать в k8s readiness/liveness) — в ./health-checks.md.
Альтернативных health-путей PM не публикует — это сознательное решение, оба k8s probe (liveness и readiness) указываются на
/health.
7. Логи и метрики¶
- Логгер —
pino, JSON, через Fastify (loggerInstance: loggerвbuildApp). - Каждая платёжная строка содержит структурированный
intentId(=trace_id),event-тег и контекст PSP/row/from/to. pino-prettyв dev-зависимостях — удобно для локального tail-а.- Drizzle включает SQL-логгер при
NODE_ENV=development.
Подробный реестр событий, уровней и интеграция с метриками/алертами — в ./monitoring.md.
8. docker-compose (локальный стек)¶
Файл projects/payment-manager/docker-compose.yml поднимает полный development-стек:
services:
postgres: # postgres:16-alpine, 127.0.0.1:5432, volume pg-data
pgbouncer: # edoburu/pgbouncer:1.22.1, 127.0.0.1:5433, pool_mode=transaction
tigerbeetle: # ghcr.io/tigerbeetle/tigerbeetle:latest, 127.0.0.1:3001
# security_opt: seccomp:unconfined (нужно для io_uring)
# entrypoint scripts/tb-init.sh — форматирует data-файл при первом старте
pm: # билдится из ./Dockerfile, 127.0.0.1:3000
# env_file: .env + WORKER_ROLES=api,outbox-worker,psp-worker, PSP_NAMES=IPPS
В compose не определён сервис
redis— он подключается из общего стека OneWallet или поднимается локально пользователем. PM ожидаетREDIS_URLв.env.
Типовой первый запуск (PM запускается отдельно через npm run dev):
docker compose up -d postgres pgbouncer tigerbeetle # инфра
npm install
npm run db:migrate # pm.* миграции
npm run db:seed # service_keys + fee_rules + routes
npm run dev # PM с hot-reload (tsx watch)
curl http://localhost:3000/health # → { "status": "ok", ... }
Запуск PM внутри контейнера (вместе со всей инфрой):
Переопределение ролей без пересборки:
9. Production checklist¶
- Свежие
SERVICE_SECRETSсгенерированы (node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"), занесены вpm.service_keyчерез seed. -
ADMIN_SECRET— не дефолтный, минимум 32 символа. -
npx drizzle-kit migrateотработал без ошибок до старта приложения. -
connectTbуспешно соединился с TB (в логах нетTigerBeetle unreachable after all retries). -
GET /health→ 200 от каждойapi-реплики. - Ровно одна реплика с
outbox-workerвWORKER_ROLES. - Сетевые порты TB/Redis/Postgres недоступны извне docker/k8s-сети.
- За nginx прокинуты заголовки
Authorization,X-Service-Id,X-Timestamp,X-Signature,X-User-Idбез модификации.
10. См. также¶
../reference/env-vars.md— полный реестр env-переменных../seed-and-migrations.md— миграции и сидирование../worker-roles.md— роли и схема развёртывания воркеров../health-checks.md— health endpoint и k8s probes../monitoring.md— логирование, метрики, алерты.