Deployment Runbook

Эта инструкция для Claude

Используй её при деплое на тестовый VPS. Без CI/CD — всё вручную через SSH.

Подключение

ssh root@185.152.93.77
cd ~/erp/erp-infrastructure

Docker Hub Rate Limit

Неавторизованные pull’ы ограничены. На сервере выполнен docker login — если образы перестают скачиваться, повторить:

docker login -u ilyakapitonovspad20@gmail.com

Disk hygiene

VPS имеет 77GB и уже один раз переполнялся (2026-04-25). Действуют две страховки: лимит логов в compose (50m × 3) и ежедневный docker-cleanup.sh в cron. Подробности: Disk Hygiene.

Обновить конкретный сервис

# 1. Pull новый код
cd ~/erp/erp-{service-repo}
git pull origin main --ff-only
 
# 2. Rebuild и restart
cd ~/erp/erp-infrastructure
docker compose build {service-name}
docker compose up -d {service-name}
 
# 3. Проверить
docker compose logs --tail=50 {service-name}
docker inspect --format '{{.State.Health.Status}}' erp-{service-name}

Или одной командой:

./scripts/deploy.sh {service-name}

Маппинг имён

Деплоится сейчас

Сервис в composeРепозиторийПорт
auth-serviceerp-auth-service3001
user-serviceerp-user-service3002
store-serviceerp-store-service3003
catalog-serviceerp-catalog-service3004
order-serviceerp-order-service3005
warehouse-serviceerp-warehouse-service3008
aggregator-serviceerp-aggregator-service3013
admin-bfferp-admin (target=bff)3020
admin-frontenderp-admin (target=frontend, nginx со статикой web/dist)80 (внутренний)
pos-bfferp-pos (monorepo — только bff/)3022
pos-frontenderp-pos-desktop (target=frontend, web-сборка SPA через Vite)80 (внутренний)

Admin split (2026-04-30)

До этой даты admin-bff был один контейнер: Fastify + раздача React SPA через @fastify/static. Теперь Dockerfile — multi-stage с двумя targets:

  • target=bff — Fastify-only API сервис
  • target=frontendnginx:alpine с собранным web/dist, ходит на admin-bff:3020 за /api/

В compose заводится два сервиса из одного Dockerfile. Внешний erp-nginx проксит /admin/*admin-frontend:80, /api/v1/admin/*admin-bff:3020. Подробнее см. PR erp-admin#3.

POS monorepo

erp-pos содержит bff/ (деплоится в compose) и mobile/ (собирается как APK через gradlew assembleRelease, не часть docker compose). Деплой касается только bff/.

Web POS (2026-04-30)

erp-pos-desktop теперь поддерживает два пути сборки из одной кодовой базы:

  • Tauri Windows .msipnpm --filter desktop tauri build локально, base=/. Для кассы на терминале с принтером.
  • Web SPADockerfile target=frontend, Vite с VITE_BASE_PATH=/pos/ и VITE_POS_BFF_URL=/pos. Деплоится в compose как pos-frontend. Без печати ESC/POS — для менеджера/официанта со смартфона/планшета. Доступно: https://erp-test.nirbi.ru/pos/.

Внешний erp-nginx роутит /pos/api/*pos-bff:3022, /pos/*pos-frontend:80. Параметризация base через env позволяет билдить оба варианта без форка кодовой базы.

Запланировано (Phase 2+)

Сервис в composeРепозиторийПорт
payment-serviceerp-payment-service3006
loyalty-serviceerp-loyalty-service3007
finance-serviceerp-finance-service3009
notification-serviceerp-notification-service3010
report-serviceerp-report-service3011
integration-serviceerp-integration-service3012
customer-bfferp-customer3021

При добавлении сервиса

Обновить обе таблицы + [[деплой-all|/deploy-all]] skill (.claude/skills/deploy-all/SKILL.md).

Обновить все запущенные сервисы

./scripts/deploy.sh

Перезапустить инфраструктуру

./scripts/deploy.sh --infra

Перезапуск PostgreSQL/Kafka

Перезапуск инфры кратковременно роняет все сервисы. Данные сохраняются в volumes.

Добавить новый сервис

1. Клонировать репозиторий

cd ~/erp
git clone <url>/erp-{service}.git

2. Создать .env файл

cd ~/erp/erp-infrastructure
 
# Для Java-сервиса:
cat > envs/{service-name}.env << 'EOF'
SERVER_PORT={port}
SPRING_PROFILES_ACTIVE=test
SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/{db_name}
SPRING_DATASOURCE_USERNAME=erp
SPRING_DATASOURCE_PASSWORD=<пароль из .env>
SPRING_REDIS_HOST=redis
SPRING_REDIS_PORT=6379
SPRING_REDIS_PASSWORD=<пароль из .env>
KAFKA_BOOTSTRAP_SERVERS=kafka:9092
JWT_SECRET=<секрет из .env>
JAVA_OPTS=-Xmx256m -Xms128m
EOF
 
# Для BFF:
cat > envs/{bff-name}.env << 'EOF'
PORT={port}
NODE_ENV=test
AUTH_SERVICE_URL=http://erp-auth-service:3001
USER_SERVICE_URL=http://erp-user-service:3002
STORE_SERVICE_URL=http://erp-store-service:3003
CATALOG_SERVICE_URL=http://erp-catalog-service:3004
WAREHOUSE_SERVICE_URL=http://erp-warehouse-service:3008
JWT_SECRET=<секрет из .env>
EOF

3. Раскомментировать в docker-compose.yml

Найти закомментированный блок сервиса или добавить новый по шаблону.

4. Раскомментировать в nginx/conf.d/default.conf

Найти закомментированный location блок для сервиса.

5. Запустить

docker compose up -d {service-name}
docker compose exec nginx nginx -s reload

6. Проверить

./scripts/status.sh
curl https://erp-test.nirbi.ru/api/{path}/health

Клонирование приватных репо на VPS

Все репо nearbyErp приватные. Для клонирования используется GitHub PAT:

cd ~/erp
git clone https://<GITHUB_PAT>@github.com/nearbyErp/<repo-name>.git

PAT хранится в памяти Claude

Не хардкодить токен в файлы. При клонировании Claude подставляет PAT из memory.

Откат сервиса

cd ~/erp/erp-{service-repo}
 
# Посмотреть последние коммиты
git log --oneline -5
 
# Откатить на конкретный коммит
git checkout {commit-hash}
 
# Пересобрать
cd ~/erp/erp-infrastructure
docker compose build {service-name}
docker compose up -d {service-name}

После отката

Не забудь вернуть на main когда фикс будет готов: git checkout main && git pull

Проверка здоровья

# Все контейнеры
./scripts/status.sh
 
# Конкретный сервис
docker compose logs --tail=50 {service-name}
 
# HTTPS endpoint
curl -s https://erp-test.nirbi.ru/health | jq
 
# PostgreSQL — проверить БД
docker compose exec postgres psql -U erp -l
 
# Redis — проверить подключение
docker compose exec redis redis-cli -a $REDIS_PASSWORD ping
 
# Kafka — список топиков
docker compose exec kafka kafka-topics.sh --bootstrap-server localhost:9092 --list

SSL-сертификат

Авто-обновляется контейнером certbot. Ручное обновление:

docker compose run --rm certbot certbot renew
docker compose exec nginx nginx -s reload

Известные особенности

JWT_SECRET — минимум 256 бит

JJWT (Spring) требует ключ >= 32 символов для HS256. Если короче — сервис не стартует:

io.jsonwebtoken.security.WeakKeyException: The specified key byte array is N bits
which is not secure enough for any JWT HMAC-SHA algorithm

Текущий секрет в .env — 48 символов, этого достаточно.

Alpine-образы не имеют curl

Java (eclipse-temurin:*-jre-alpine) и Node (node:*-alpine) образы не содержат curl. Healthcheck в compose использует:

  • Java-сервисы: nc -z localhost {port} (TCP проверка)
  • Node-сервисы: nc -z 127.0.0.1 {port} (TCP проверка)

Не HTTP healthcheck

TCP проверка порта подтверждает что процесс жив и слушает, но не проверяет готовность приложения. Spring Security блокирует /actuator/health с 403 без токена. Для полноценного healthcheck нужно открыть actuator в SecurityConfig.

Fastify — binding на 0.0.0.0

Fastify 5.x в callback-стиле app.listen({ host: "0.0.0.0" }, cb) может игнорировать host. Использовать promise-стиль:

app.listen({ port: config.port, host: "0.0.0.0" }).then(...)

Иначе BFF слушает только на 127.0.0.1 и nginx не может к нему обратиться через Docker network.

Certbot — первичное получение сертификата

docker compose run certbot не работает — entrypoint из compose (renew loop) перехватывает команду. Использовать docker run напрямую:

docker run --rm \
  -v erp-infrastructure_certbot_webroot:/var/www/certbot \
  -v erp-infrastructure_certbot_certs:/etc/letsencrypt \
  --network erp-infrastructure_erp-network \
  certbot/certbot:latest \
  certonly --webroot --webroot-path=/var/www/certbot \
  --email admin@nirbi.ru --agree-tos --no-eff-email \
  -d erp-test.nirbi.ru

После получения — docker compose exec nginx nginx -s reload.

Авто-обновление работает через контейнер certbot (renew каждые 12 часов).

Spring Boot start_period

Java-сервисы стартуют 8-15 секунд (скачивание зависимостей, Liquibase миграции). В compose задан start_period: 90s — healthcheck не считается failed первые 90 секунд. Если после 90s сервис всё ещё starting — смотреть логи.

Типичные проблемы

Сервис не стартует — OOM

docker compose logs {service} | grep -i "killed\|oom\|memory"
# Решение: уменьшить Xmx в envs/{service}.env или остановить ненужные сервисы

Kafka не отвечает

docker compose restart kafka
# Подождать 30 сек, проверить:
docker compose exec kafka kafka-broker-api-versions.sh --bootstrap-server localhost:9092

PostgreSQL — миграция упала

docker compose logs {service} | grep -i "liquibase\|migration\|changelog"
# Обычно: фикс миграции в коде → передеплой сервиса

Диск заполнен

# Проверить
df -h /
docker system df
 
# Почистить старые образы
docker image prune -a --filter "until=168h"
docker volume prune

Nginx 502 Bad Gateway

Сервис упал или ещё не стартовал:

docker compose ps {service}
docker compose logs --tail=30 {service}

Локальная разработка фронта (бэкенды на сервере)

Для работы над erp-admin (BFF + React) без локальных бэкендов.

1. SSH tunnel к бэкендам

В отдельном терминале:

ssh -L 3002:localhost:3002 -L 3003:localhost:3003 root@185.152.93.77

Порты сервисов на VPS проброшены на 127.0.0.1 — доступны только через туннель, не из интернета.

2. Запуск фронта

cd erp-admin
pnpm dev
  • Vite на :5173 — React с hot-reload, проксирует /api → BFF
  • BFF на :3020 — Fastify, обращается к localhost:3002 / localhost:3003 через туннель

3. Открыть в браузере

http://localhost:5173

Без туннеля

BFF стартует, но запросы к бэкендам будут падать с ECONNREFUSED. Фронт покажет ошибки при загрузке данных.

Для Claude Code

Claude Code в репозитории erp-admin может:

  1. Открыть SSH туннель: ssh -fN -L 3002:localhost:3002 -L 3003:localhost:3003 root@185.152.93.77 (фоном)
  2. Запустить pnpm dev
  3. Разрабатывать BFF и фронт с полноценными бэкендами

.env файлы на сервере

Глобальный .env и per-service envs/*.env находятся в ~/erp/erp-infrastructure/ на VPS. В git не хранятся (.gitignore).

Шаблон .env для Java-сервиса:

SERVER_PORT={port}
SPRING_PROFILES_ACTIVE=test
DATABASE_URL=jdbc:postgresql://postgres:5432/{db_name}
DB_USERNAME=erp
DB_PASSWORD=<из .env>
JWT_SECRET=<из .env, минимум 32 символа>
SERVICE_TOKEN=<из .env>
STORE_SERVICE_URL=http://erp-store-service:3003
JAVA_OPTS=-Xmx256m -Xms128m

Шаблон .env для BFF:

PORT={port}
NODE_ENV=production
AUTH_SERVICE_URL=http://erp-auth-service:3001
USER_SERVICE_URL=http://erp-user-service:3002
STORE_SERVICE_URL=http://erp-store-service:3003
SERVICE_TOKEN=<из .env>

Пароли

Реальные пароли хранятся только на сервере в .env файлах и в памяти Claude. Не коммитить в git.

Ссылки