Перейти к содержимому

Инфраструктура и развёртывание

Как устроен прод CrewsForge: где что крутится, как сервисы находят друг друга, как разворачивать с нуля.

Важно: тестового контура нет. Всё, что здесь описано, — прод.


Один VPS 145.63.138.245, на нём Dokploy (панель деплоя) с Traefik в роли реверс-прокси. Все сервисы — Docker-контейнеры в общей сети dokploy-network; Traefik терминирует TLS и маршрутизирует по доменам.

Интернет
│ HTTPS (Let's Encrypt)
Traefik ──────────────────────────────────────────┐
│ auth-api.crewsforge.com → auth-api │
│ user-api.crewsforge.com → user-api │
│ project-api… → project-api │ сеть dokploy-network
│ admin-api… → admin-api │
│ ai-agent… → ai-agent-service │
│ docs.crewsforge.online → docs-site │
└──────────────────────────────────────────────┘
┌──────────────┼───────────────┐
▼ ▼ ▼
PostgreSQL Redis RabbitMQ

Панель Dokploy: https://dp.crewsforge.com


Разделено по слоям, а не свалено в кучу:

ПроектЧто внутриЗачем отдельно
infrapostgres, redis, rabbitmqОбщие зависимости. Живут дольше приложений, деплоятся отдельно
backendauth-api, user-api, project-api, admin-api, ai-agent-serviceМикросервисы из монорепо crewsforge-back
frontendweb (app.crewsforge.com)Отдельный репозиторий crewsforge-web
docsdocs-siteСайт документации

СервисОбразВнутренний хостПорт
PostgreSQLpostgres:16postgres-copy-auxiliary-card-vqwwez5432
Redisredis:7redis-reboot-neural-transmitter-23xewa6379
RabbitMQrabbitmq:4.1-managementrabbitmq5672

Как адресуются. Внутри dokploy-network контейнеры находят друг друга по имени сервиса — это и есть хост в строке подключения. Наружу ни одна из баз не опубликована: доступ только изнутри сети или через SSH-туннель.

Гоча с RabbitMQ. Шаблон Dokploy поднимает его в собственной изолированной сети, из-за чего приложения его не видят. Compose поправлен: добавлена dokploy-network как external, алиас rabbitmq. Если будешь пересоздавать rabbit из шаблона — проверь это снова.

Management-UI RabbitMQ (15672) наружу не опубликован намеренно. Нужен — прокинь туннелем:

Окно терминала
ssh -L 15672:rabbitmq:15672 root@145.63.138.245

Полный список с комментариями — в dokploy/.env.example.

Как это работает:

  1. Значения задаются в Dokploy у каждого сервиса (вкладка Environment).
  2. При деплое Dokploy генерирует .env рядом с compose-файлом — то есть в code/dokploy/.env.
  3. Compose передаёт его в контейнер через env_file: .env (путь резолвится относительно каталога compose-файла).
  4. NestJS читает process.env, валидирует через Joi.

Два следствия, которые важно понимать:

  • Секреты не попадают в образ. Раньше .env копировался в образ на этапе сборки — секреты оседали в слоях, а смена любой переменной требовала полной пересборки (npm ci + prisma generate + build). Теперь env приезжает в рантайме: поменял значение → редеплой без пересборки.
  • Нет переменной — сервис не стартует. Joi валидирует конфиг при запуске и падает с явной ошибкой. Это не баг, а защита от «поднялся, но молча не работает».

У каждого сервиса свой APP_PORT (внутри контейнера):

СервисAPP_PORT
auth-api3000
user-api3001
project-api3002
admin-api3003
ai-agent-service3004

Все backend-сервисы собираются одним dokploy/Dockerfile.node (multi-stage):

  1. base — ставит зависимости (npm ci --omit=dev), пересобирает sharp под musl/Alpine, генерирует Prisma-клиент, собирает нужное приложение через npm run build:<SCRIPT_NAME>.
  2. production — копирует только node_modules, dist, package.json; запускает через PM2 (pm2-runtime, число процессов из PM2_INSTANCE_NUMBER).

Какое приложение собирать, определяет build-arg SCRIPT_NAME (например auth-api), заданный в compose-файле сервиса.

Источник деплоя: GitHub crewsforge/crewsforge-back, ветка main, автодеплой по push. У каждого сервиса свой compose: dokploy/docker-compose.<сервис>.yml.

Портов на хост сервисы не публикуют — маршрутизацией занимается Traefik. Это убирает конфликты портов и не даёт обойти TLS.


Схема одна на весь монорепо: apps/core-api/prisma/schema.prisma. core-api — не запускаемый сервис, а владелец схемы и миграций.

Развёртывание на чистой базе:

Окно терминала
# Миграции
npx prisma migrate deploy --schema=./apps/core-api/prisma/schema.prisma

Роли сидами НЕ создаются. UserSeeder пустой. Роли CUSTOMER и EMPLOYEE создаются сами при первой регистрации — через connectOrCreate с фиксированными UUID из role.constants.ts. Роль ADMIN работает так же — регистрация админа создаёт её через connectOrCreate.

geo.seed.ts (страны, таймзоны, локации) — опциональный, тянет данные из внешнего API RESTCOUNTRIES_URL. Запускается через nx run core-api:prisma:seed:run.

Гоча. Цепочка миграций долго не применялась на чистую базу: projects и walrider_* не создавала ни одна миграция — их налили через db push, а следующая миграция только ALTER-ила. Восстановлено отдельной миграцией. Если увидишь relation ... does not exist при migrate deploy — ищи ту же болезнь.


Каждому сервису — свой поддомен, сертификаты Let’s Encrypt выпускаются Traefik автоматически.

СервисДомен
Лендингcrewsforge.com (+ www → редирект, см. 7.1)
Фронтендapp.crewsforge.com
Authauth-api.crewsforge.com
Usersuser-api.crewsforge.com
Projectsproject-api.crewsforge.com
Adminadmin-api.crewsforge.com
AI-агентai-agent.crewsforge.com
Документацияdocs.crewsforge.online
Панель Dokploydp.crewsforge.com

Условие: для каждого поддомена нужна A-запись на 145.63.138.245. Без неё Let’s Encrypt не пройдёт HTTP-01 проверку и сертификат не выпустится.

Гоча, на которой легко потерять час. Traefik роутит строго по Host(). Запрос на адрес, для которого роутера нет (например авто-домен *.sslip.io), вернёт 404 от Traefik и невалидный сертификат — при полностью живом приложении. Прежде чем винить сборку, проверь напрямую в контейнер:

Окно терминала
curl -s -o /dev/null -w "%{http_code}" http://localhost:<APP_PORT>/

www.crewsforge.com отдаёт 301 на https://crewsforge.com. Сделано через middleware Traefik, который живёт в file-провайдере: /etc/dokploy/traefik/dynamic/middlewares.yml.

Почему именно так, а не «просто отдавать сайт на обоих адресах»: два одинаковых сайта на разных адресах — дубль контента, ссылочный вес размывается. 301 (permanent: true, не 302) передаёт вес апексу и кешируется браузером.

‼ Этого файла нет в git — он существует только на сервере. Потеряем VPS — потеряем и конфиг. Восстанавливать через Dokploy → Settings → Traefik либо API settings.updateMiddlewareTraefikConfig. По SSH файл править не нужно:

http:
middlewares:
redirect-to-https: # ставит сам Dokploy при установке
redirectScheme:
scheme: https
permanent: true
redirect-to-apex: # добавлен вручную
redirectRegex:
regex: '^https?://www[.](.+)'
replacement: 'https://${1}'
permanent: true

Затем у домена www.* в Dokploy указать middleware redirect-to-apex@file. Правило generic — сработает для любого домена, не только crewsforge.com.

Домен www при этом должен быть заведён как полноценный, с сертификатом. Без сертификата браузер упрётся в ошибку TLS до того, как получит редирект.

Две грабли, на которых я наступил:

  1. Только одинарные кавычки. В двойных YAML считает \. escape-последовательностью и падает: yaml: found unknown escape character.
  2. [.] вместо \. — backslash не выживает при записи через API Dokploy, его срезает. [.] — та же литеральная точка, но без единого escape.

Проверка после правки обязательна, иначе сломанный конфиг заметишь не сразу:

Окно терминала
docker logs dokploy-traefik --since 1m | grep -i error # пусто = конфиг принят
curl -sL -o /dev/null -w '%{url_effective} %{num_redirects}' http://www.crewsforge.com

Хорошая новость: при битом файле Traefik не падает — отвергает изменение и продолжает работать на последней валидной конфигурации. Простоя не будет, но и правка не применится.


ЧтоЗачемБез него
SMTPКоды подтверждения на почтуНе работает регистрация и вход админа (он входит без пароля, только по коду)
S3Аватары, медиа портфолиоНе грузятся файлы
Агент WalriderВнешний ИИ-агент, потребитель очередей RabbitMQЧат примет сообщение и сохранит его, но ответа не будет

Walrider живёт вне этого репозитория. ai-agent-service — только шлюз: пишет запрос в ai.requests, слушает ai.responses, отдаёт результат в WebSocket.


  1. Поднять infra: PostgreSQL, Redis, RabbitMQ (rabbit — проверить сеть, п.3).
  2. Прописать DNS A-записи на все поддомены.
  3. Сгенерировать JWT-секреты: openssl rand -hex 32 × 6.
  4. Задать env каждому сервису в Dokploy (см. .env.example).
  5. Накатить миграции и сиды (п.6).
  6. Создать сервисы в проекте backend из репозитория, ветка main.
  7. Привязать домены, включить Let’s Encrypt.
  8. Проверить: Swagger отвечает, БД/Redis/Rabbit подключились, проходит регистрация.

Правки конфигурации — только через панель Dokploy или её API. Dokploy хранит состояние в своей БД и перегенерирует файлы на диске при каждом деплое: ручные правки по SSH затираются и не видны в UI.

По SSH — только чтение:

Окно терминала
docker ps # что запущено
docker logs <container> --tail 100 # логи
docker inspect <container> --format '{{json .NetworkSettings.Networks}}' # сети
docker exec <postgres> pg_isready -U crewsforge
docker exec <redis> redis-cli -a <пароль> ping