Инфраструктура и развёртывание
Как устроен прод CrewsForge: где что крутится, как сервисы находят друг друга, как разворачивать с нуля.
Важно: тестового контура нет. Всё, что здесь описано, — прод.
1. Общая картина
Заголовок раздела «1. Общая картина»Один 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
2. Структура проектов в Dokploy
Заголовок раздела «2. Структура проектов в Dokploy»Разделено по слоям, а не свалено в кучу:
| Проект | Что внутри | Зачем отдельно |
|---|---|---|
| infra | postgres, redis, rabbitmq | Общие зависимости. Живут дольше приложений, деплоятся отдельно |
| backend | auth-api, user-api, project-api, admin-api, ai-agent-service | Микросервисы из монорепо crewsforge-back |
| frontend | web (app.crewsforge.com) | Отдельный репозиторий crewsforge-web |
| docs | docs-site | Сайт документации |
3. Инфраструктурные сервисы
Заголовок раздела «3. Инфраструктурные сервисы»| Сервис | Образ | Внутренний хост | Порт |
|---|---|---|---|
| PostgreSQL | postgres:16 | postgres-copy-auxiliary-card-vqwwez | 5432 |
| Redis | redis:7 | redis-reboot-neural-transmitter-23xewa | 6379 |
| RabbitMQ | rabbitmq:4.1-management | rabbitmq | 5672 |
Как адресуются. Внутри dokploy-network контейнеры находят друг друга по имени сервиса — это и есть хост в строке подключения. Наружу ни одна из баз не опубликована: доступ только изнутри сети или через SSH-туннель.
Гоча с RabbitMQ. Шаблон Dokploy поднимает его в собственной изолированной сети, из-за чего приложения его не видят. Compose поправлен: добавлена dokploy-network как external, алиас rabbitmq. Если будешь пересоздавать rabbit из шаблона — проверь это снова.
Management-UI RabbitMQ (15672) наружу не опубликован намеренно. Нужен — прокинь туннелем:
ssh -L 15672:rabbitmq:15672 root@145.63.138.2454. Переменные окружения
Заголовок раздела «4. Переменные окружения»Полный список с комментариями — в dokploy/.env.example.
Как это работает:
- Значения задаются в Dokploy у каждого сервиса (вкладка Environment).
- При деплое Dokploy генерирует
.envрядом с compose-файлом — то есть вcode/dokploy/.env. - Compose передаёт его в контейнер через
env_file: .env(путь резолвится относительно каталога compose-файла). - NestJS читает
process.env, валидирует через Joi.
Два следствия, которые важно понимать:
- Секреты не попадают в образ. Раньше
.envкопировался в образ на этапе сборки — секреты оседали в слоях, а смена любой переменной требовала полной пересборки (npm ci + prisma generate + build). Теперь env приезжает в рантайме: поменял значение → редеплой без пересборки. - Нет переменной — сервис не стартует. Joi валидирует конфиг при запуске и падает с явной ошибкой. Это не баг, а защита от «поднялся, но молча не работает».
У каждого сервиса свой APP_PORT (внутри контейнера):
| Сервис | APP_PORT |
|---|---|
| auth-api | 3000 |
| user-api | 3001 |
| project-api | 3002 |
| admin-api | 3003 |
| ai-agent-service | 3004 |
5. Сборка и деплой
Заголовок раздела «5. Сборка и деплой»Все backend-сервисы собираются одним dokploy/Dockerfile.node (multi-stage):
- base — ставит зависимости (
npm ci --omit=dev), пересобираетsharpпод musl/Alpine, генерирует Prisma-клиент, собирает нужное приложение черезnpm run build:<SCRIPT_NAME>. - 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.
6. База данных
Заголовок раздела «6. База данных»Схема одна на весь монорепо: 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 — ищи ту же болезнь.
7. Домены и TLS
Заголовок раздела «7. Домены и TLS»Каждому сервису — свой поддомен, сертификаты Let’s Encrypt выпускаются Traefik автоматически.
| Сервис | Домен |
|---|---|
| Лендинг | crewsforge.com (+ www → редирект, см. 7.1) |
| Фронтенд | app.crewsforge.com |
| Auth | auth-api.crewsforge.com |
| Users | user-api.crewsforge.com |
| Projects | project-api.crewsforge.com |
| Admin | admin-api.crewsforge.com |
| AI-агент | ai-agent.crewsforge.com |
| Документация | docs.crewsforge.online |
| Панель Dokploy | dp.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>/7.1. Редирект www → апекс
Заголовок раздела «7.1. Редирект www → апекс»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 до того, как получит редирект.
Две грабли, на которых я наступил:
- Только одинарные кавычки. В двойных YAML считает
\.escape-последовательностью и падает:yaml: found unknown escape character. [.]вместо\.— 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 не падает — отвергает изменение и продолжает работать на последней валидной конфигурации. Простоя не будет, но и правка не применится.
8. Внешние зависимости
Заголовок раздела «8. Внешние зависимости»| Что | Зачем | Без него |
|---|---|---|
| SMTP | Коды подтверждения на почту | Не работает регистрация и вход админа (он входит без пароля, только по коду) |
| S3 | Аватары, медиа портфолио | Не грузятся файлы |
| Агент Walrider | Внешний ИИ-агент, потребитель очередей RabbitMQ | Чат примет сообщение и сохранит его, но ответа не будет |
Walrider живёт вне этого репозитория. ai-agent-service — только шлюз: пишет запрос в ai.requests, слушает ai.responses, отдаёт результат в WebSocket.
9. Порядок развёртывания с нуля
Заголовок раздела «9. Порядок развёртывания с нуля»- Поднять
infra: PostgreSQL, Redis, RabbitMQ (rabbit — проверить сеть, п.3). - Прописать DNS A-записи на все поддомены.
- Сгенерировать JWT-секреты:
openssl rand -hex 32× 6. - Задать env каждому сервису в Dokploy (см.
.env.example). - Накатить миграции и сиды (п.6).
- Создать сервисы в проекте
backendиз репозитория, веткаmain. - Привязать домены, включить Let’s Encrypt.
- Проверить: Swagger отвечает, БД/Redis/Rabbit подключились, проходит регистрация.
10. Диагностика
Заголовок раздела «10. Диагностика»Правки конфигурации — только через панель 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 crewsforgedocker exec <redis> redis-cli -a <пароль> ping