Перейти к содержанию

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.

Сборка вручную:

cd projects/payment-manager
docker build -t onewallet/payment-manager:local .

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:

GET /health
  • 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 внутри контейнера (вместе со всей инфрой):

docker compose build pm
docker compose up -d postgres pgbouncer tigerbeetle
docker compose up pm

Переопределение ролей без пересборки:

WORKER_ROLES=api docker compose up pm
WORKER_ROLES=psp-worker PSP_NAMES=IPPS docker compose up 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. См. также