Skip to main content

Services Deployment

Создание и настройка каждого сервиса в Dokploy. Порядок деплоя критичен.


1. Порядок деплоя

Сервисы зависят друг от друга — деплой строго последовательный:

ПорядокСервисПричина зависимости
1BackendВсе зависят от API
2FrontendНужен VITE_API_URL
3AdminНужен VITE_API_URL
4Redirect ServiceНужна та же БД
5Static NginxShared volume с Backend
6Docs SiteНужен API для OpenAPI spec

2. Общая процедура для каждого сервиса

В Dokploy UI для каждого сервиса:

  1. Projects → Выбрать проект "goLoot" (или создать)
  2. New ServiceApplication
  3. Настроить Provider (вкладка General, верхняя секция):
    • Source: GitHub → выбрать репозиторий
    • Branch: main
    • Build Path: / (корень репо, одинаково для всех)
    • Trigger Type: On Push
    • Watch Paths: зависит от сервиса (см. таблицы ниже)
  4. Настроить Build Type (вкладка General, нижняя секция):
    • Build Type: Dockerfile
    • Docker File: путь к Dockerfile (см. таблицу ниже)
    • Docker Context Path: . (корень репо, одинаково для всех)
  5. Environment Variables: задать переменные (вкладка Environment)
  6. Domains: привязать домен с HTTPS (вкладка Domains)
  7. Deploy

Build Path и Docker Context Path

Оба поля = корень репозитория

В Dokploy есть два похожих поля — оба должны указывать на корень монорепо:

ПолеГде находитсяЗначениеНазначение
Build PathGeneral → Provider/Базовый путь в репозитории
Docker Context PathGeneral → Build Type.Контекст для Docker build

Все Dockerfiles используют контекст . (корень монорепо), потому что:

  • Backend и Redirect Service копируют shared/ и backend/prisma/
  • Frontend и Admin копируют shared/
  • Поэтому Docker Context Path всегда ., а Docker File — путь к конкретному Dockerfile

3. Сервис: Backend

Настройки в Dokploy

ПараметрЗначениеГде в UI
NamebackendGeneral → Provider
Build Path/General → Provider
Docker Filebackend/DockerfileGeneral → Build Type
Docker Context Path.General → Build Type
Port4000Domains

Domain

HostPortHTTPS
api.goloot.online4000Let's Encrypt

Environment Variables

Полный список в Environment Variables. Минимум для запуска:

NODE_ENV=production
PORT=4000
HOST=0.0.0.0

# Database
DB_ENV=prod
DATABASE_URL_PROD=postgresql://goloot:PASSWORD@postgresql:5432/goloot

# JWT
JWT_SECRET=your-secure-jwt-secret-min-32-chars
JWT_EXPIRES_IN=7d

# Telegram
TELEGRAM_BOT_TOKEN=your-bot-token
TELEGRAM_BOT_USERNAME=YourBotUsername
TELEGRAM_APP_NAME=your_app_name
TELEGRAM_MINI_APP_URL=https://goloot.online
TELEGRAM_WEBHOOK_URL=https://api.goloot.online/api/telegram/webhook

# URLs
STATIC_URL=https://static.goloot.online
WEB_APP_URL=https://goloot.online
REDIRECT_START_URL=https://start.goloot.online
REDIRECT_PUSH_URL=https://r.goloot.online

# Redis (опционально)
REDIS_URL=redis://:PASSWORD@redis:6379

# Quiz Generation (SQLite)
QUIZ_SQLITE_PATH=/var/data/goloot/rust_data.db

# Logging
LOG_LEVEL=info

Health Check

Health Check настраивается через Swarm Settings в Dokploy:

  1. Dokploy → Backend Service → Advanced
  2. Найти секцию Cluster Settings → нажать кнопку Swarm Settings
  3. В левом меню выбрать Health Check
  4. Заполнить поля:

Test Commands — нажать Add Command дважды и добавить:

#Значение
1CMD-SHELL
2wget --no-verbose --tries=1 --spider http://localhost:4000/health
wget, не curl!

В production-образе Backend установлен wget, но не curl (Alpine-образ с минимальными зависимостями). Если использовать curl — health check будет падать с ошибкой "not found".

Остальные поля:

ПолеЗначениеОписание
Interval3000000000030 сек между проверками
Timeout1000000000010 сек на ответ
Start Period3000000000030 сек на запуск (Prisma init, plugins)
Retries33 неудачи → контейнер помечается unhealthy
Наносекунды

Docker Swarm принимает время в наносекундах. 1 секунда = 1,000,000,000 нс. Поэтому 30 секунд = 30000000000.

  1. Нажать Save Health Check, затем Redeploy

Проверка после деплоя:

curl -s https://api.goloot.online/health
# {"status":"ok","timestamp":"..."}

Volumes

В Dokploy → Backend Service → AdvancedVolumes → нажать Add Volume.

Нужно добавить 2 volume разных типов:

Volume 1: Static files (shared с Static Nginx)

ПолеЗначение
Mount TypeVolume Mount
Volume Namegoloot_static
Mount Path/static
Заливка static files на VPS (пошагово)

Перед первым деплоем нужно заполнить goloot_static volume изображениями (предметы Rust, баннеры, иконки и т.д.). Без этого статика не будет отображаться.

1. Создать Docker volume на VPS

docker volume create goloot_static

2. Архивировать static на локальной машине

cd /path/to/goloot/tmp && tar -czf static.tar.gz static/

Архив включает images/ (предметы, баннеры, аватары, квизы) и guides/. Размер ~83 MB.

3. Скопировать архив на VPS

scp static.tar.gz root@YOUR_SERVER_IP:/tmp/

4. Распаковать в Docker volume и выдать права

# Распаковать (--strip-components=1 убирает папку static/ из архива)
tar -xzf /tmp/static.tar.gz -C /var/lib/docker/volumes/goloot_static/_data/ --strip-components=1

# Выдать права (1001:1001 = nodejs user в Backend-контейнере)
chown -R 1001:1001 /var/lib/docker/volumes/goloot_static/_data/

# Проверить
ls -la /var/lib/docker/volumes/goloot_static/_data/
# Ожидаемый вывод: guides/ images/ — владелец 1001:1001

# Удалить архив
rm /tmp/static.tar.gz
Владелец 1001:1001 обязателен

Backend-контейнер работает под пользователем nodejs (UID=1001). Если владелец файлов другой (например ubuntu:docker после SCP) — Backend не сможет записывать новые файлы (аватары, баннеры, кейсы). Всегда выполняйте chown -R 1001:1001 после заливки.

Volume 2: SQLite database (quiz generation)

ПолеЗначение
Mount TypeBind Mount
Host Path/var/data/goloot
Mount Path/var/data/goloot
SQLite bind mount

Файл /var/data/goloot/rust_data.db (~29 MB) содержит данные предметов Rust для генерации квизов. Без него quiz generation не работает.

Владелец: 1001:1001 (совпадает с nodejs пользователем в контейнере). Режим: read-only — Backend только читает эту базу.

Подготовка SQLite на хосте (пошагово)

Почему 1001:1001?

Backend-контейнер работает под непривилегированным пользователем nodejs (UID=1001, GID=1001), заданным в backend/Dockerfile. Файлы, смонтированные через bind mount, сохраняют владельца с хоста. Если владелец не совпадает — контейнер не сможет прочитать файл и quiz generation упадёт с ошибкой.

Первоначальная настройка

# 1. Создать директорию на хосте
sudo mkdir -p /var/data/goloot

# 2. Скопировать файл базы (из локальной машины или другого источника)
# Вариант A: с локальной машины через scp
scp quizzes/data/rust_data.db root@YOUR_SERVER_IP:/var/data/goloot/

# Вариант B: файл уже на сервере — просто скопировать
sudo cp /path/to/rust_data.db /var/data/goloot/

# 3. Назначить владельца — ОБЯЗАТЕЛЬНО
sudo chown -R 1001:1001 /var/data/goloot

# 4. Проверить
ls -la /var/data/goloot/rust_data.db
# Ожидаемый вывод:
# -rw-r--r-- 1 1001 1001 29M ... rust_data.db

Обновление файла базы

При обновлении rust_data.db (новые предметы, патч Rust):

# 1. Загрузить новый файл
scp quizzes/data/rust_data.db root@YOUR_SERVER_IP:/var/data/goloot/

# 2. Восстановить владельца (scp сбрасывает на root:root)
sudo chown 1001:1001 /var/data/goloot/rust_data.db

# 3. Перезапуск контейнера НЕ нужен — SQLite открывается при каждом запросе

Диагностика

Если quiz generation не работает — проверь:

# Файл существует?
ls -la /var/data/goloot/rust_data.db

# Владелец правильный? (должно быть 1001 1001)
stat -c '%u:%g' /var/data/goloot/rust_data.db

# Bind mount подключён в контейнере?
# (в Dokploy → Backend → Advanced → Volumes должен быть /var/data/goloot)
docker exec <container_id> ls -la /var/data/goloot/rust_data.db

# Env переменная задана?
# QUIZ_SQLITE_PATH=/var/data/goloot/rust_data.db
СимптомПричинаРешение
SQLITE_CANTOPENФайл не найден или нет правchown 1001:1001 + проверить bind mount
no such table: itemsФайл пустой или повреждёнЗаново скопировать rust_data.db
Quiz generation молча не работаетQUIZ_SQLITE_PATH не заданДобавить env переменную в Dokploy
Структура папок в goloot_static volume
/static/
├── images/ # Основная папка
│ ├── grain.webp # Текстура UI (grain overlay)
│ ├── steam.webp # Steam иконка
│ ├── SkillTree/ # Иконки дерева навыков Rust
│ │ ├── Build_Craft/
│ │ ├── Combat/
│ │ ├── Cooking/
│ │ ├── Harvesting/
│ │ ├── Medical/
│ │ ├── Mining/
│ │ ├── Raiding/
│ │ ├── Scavenging/
│ │ ├── Skinning/
│ │ ├── Team/
│ │ ├── Underwater/
│ │ ├── Vehicles/
│ │ └── Woodcutting/
│ ├── opengraph/
│ │ ├── referrals/ # OG-превью для реферальных ссылок
│ │ ├── utm/ # OG-превью для UTM кампаний
│ │ └── content/ # OG-превью для контента
│ ├── banners/ # Рекламные баннеры (загрузка через админку)
│ ├── push/ # Изображения для push-уведомлений
│ ├── quizzes/ # Изображения для вопросов квизов
│ ├── cases/ # Изображения кейсов (динамически, Admin API)
│ ├── avatars/ # Telegram аватары (кэш 24ч)
│ └── items/ # Игровые предметы (см. ниже)
│ ├── currency/ # Иконки валют
│ │ ├── scrap.webp # Иконка scrap
│ │ └── xp.webp # Иконка XP
│ ├── badges/ # Badge overlays для предметов
│ │ ├── blueprintbase.webp # Badge для BLUEPRINT
│ │ └── fragment_*_tier_*.webp # Badge для FRAGMENT (по тирам)
│ ├── buffs/ # Иконки баффов
│ │ ├── scrap.webp # Бафф на scrap
│ │ ├── xp.webp # Бафф на XP
│ │ └── shield.webp # Бафф на защиту
│ ├── resources/ # Иконки ресурсов крафта (17 файлов)
│ ├── rust/ # Предзаполненные изображения Rust-предметов
│ │ ├── Containers/
│ │ ├── Scientists/
│ │ ├── Animals/
│ │ ├── Weapons/
│ │ ├── Tools/
│ │ ├── Attire/
│ │ ├── Medical/
│ │ ├── Food/
│ │ ├── Resources/
│ │ ├── Construction/
│ │ ├── Electrical/
│ │ ├── Components/
│ │ ├── Ammo/
│ │ ├── Items/
│ │ ├── Traps/
│ │ ├── Fun/
│ │ └── Misc/
│ └── skin/ # Скины (загрузка через Admin API)
│ ├── clothing/
│ ├── armor/
│ ├── weapon/
│ └── other/
└── guides/ # Гайды (создаются при деплое через Dockerfile)

Источники данных в items/:

  • items/rust/ — предзаполненные PNG-изображения Rust-предметов (загружены вручную)
  • items/skin/ — создаются динамически через Admin API при загрузке скинов
  • items/currency/, items/badges/, items/buffs/, items/resources/ — статичные ассеты UI

Права: Владелец всех файлов: 1001:1001 (nodejs user в контейнере).

Watch Paths

В Dokploy → AdvancedWatch Paths (для автодеплоя):

backend/**
shared/**

После первого деплоя

Применить миграции и seeds — см. Database Setup: Инициализация.


4. Сервис: Frontend

Настройки в Dokploy

ПараметрЗначениеГде в UI
NamefrontendGeneral → Provider
Build Path/General → Provider
Docker Filefrontend/DockerfileGeneral → Build Type
Docker Context Path.General → Build Type
Port80Domains

Domain

HostPortHTTPS
goloot.online80Let's Encrypt

Build-time Variables

Frontend использует build-time переменные (ARG в Dockerfile). В Dokploy это Build-time Variables (не Environment):

VITE_API_URL=https://api.goloot.online
VITE_TELEGRAM_BOT_USERNAME=YourBotUsername
VITE_TELEGRAM_CHANNEL_USERNAME=YourChannelUsername
VITE_REDIRECT_START_URL=https://start.goloot.online
Полный список build-time variables
VariableОписаниеПример
VITE_API_URLBackend API URLhttps://api.goloot.online
VITE_TELEGRAM_BOT_USERNAMEUsername ботаGoLootBot
VITE_TELEGRAM_CHANNEL_USERNAMEUsername каналаgoloot_channel
VITE_SEQUENTIAL_API_TIMEOUTТаймаут API (мс)5000
VITE_SEQUENTIAL_TOTAL_TIMEOUTОбщий таймаут (мс)15000
VITE_AD_FETCH_TIMEOUTТаймаут рекламы (мс)3000
VITE_FAVORITES_CACHE_TTLTTL кэша избранного300000
VITE_POPULARITY_NEW_THRESHOLDПорог "new" популярности5
VITE_POPULARITY_NORMAL_THRESHOLDПорог "normal"20
VITE_POPULARITY_HOT_THRESHOLDПорог "hot"50
VITE_POPULARITY_EPIC_THRESHOLDПорог "epic"100
VITE_REDIRECT_START_URLURL redirect-сервиса (referral/UTM)https://start.goloot.online
VITE_BANNER_HIDE_DURATIONСкрытие баннера (мс)86400000

Watch Paths

frontend/**
shared/**

5. Сервис: Admin

Настройки в Dokploy

ПараметрЗначениеГде в UI
NameadminGeneral → Provider
Build Path/General → Provider
Docker Fileadmin/DockerfileGeneral → Build Type
Docker Context Path.General → Build Type
Port80Domains

Domain

HostPortHTTPS
admin.goloot.online80Let's Encrypt

Build-time Variables

VITE_API_URL=https://api.goloot.online
VITE_STATIC_URL=https://static.goloot.online
VITE_REDIRECT_START_URL=https://start.goloot.online
VITE_REDIRECT_PUSH_URL=https://r.goloot.online
VITE_GRAFANA_URL=https://grafana.goloot.online

Watch Paths

admin/**
shared/**

6. Сервис: Redirect Service

Настройки в Dokploy

ПараметрЗначениеГде в UI
Nameredirect-serviceGeneral → Provider
Build Path/General → Provider
Docker Fileredirect-service/DockerfileGeneral → Build Type
Docker Context Path.General → Build Type
Port3001Domains

Domains

HostPortHTTPSНазначение
start.goloot.online3001Let's EncryptРеферальные/UTM ссылки (user-facing)
r.goloot.online3001Let's EncryptPush tracking (internal)
Два домена — один сервис

Redirect Service обслуживает оба домена:

  • start.goloot.online — для ссылок, которые видит пользователь (рефералы, UTM). UX: "start" = начни здесь
  • r.goloot.online — для технических ссылок (push-рассылки). Пользователь их не видит напрямую

Environment Variables

NODE_ENV=production
PORT=3001
HOST=0.0.0.0
LOG_LEVEL=info
DATABASE_URL=postgresql://goloot:PASSWORD@postgresql:5432/goloot
STATIC_URL=https://static.goloot.online
START_URL=https://start.goloot.online
TELEGRAM_BOT_USERNAME=YourBotUsername
TELEGRAM_APP_NAME=your_app_name
DATABASE_URL (не DATABASE_URL_PROD)

Redirect Service использует DATABASE_URL напрямую (не DATABASE_URL_PROD), потому что у него простой config без env-switching.

Watch Paths

redirect-service/**
backend/prisma/**

7. Сервис: Static Nginx

Настройки в Dokploy

ПараметрЗначениеГде в UI
Namestatic-nginxGeneral → Provider
Build Path/General → Provider
Docker Filestatic-nginx/DockerfileGeneral → Build Type
Docker Context Path.General → Build Type
Port80Domains

Domain

HostPortHTTPS
static.goloot.online80Let's Encrypt

Volume

Static Nginx использует Docker volume для хранения файлов, общий с Backend.

В Dokploy → Static Nginx Service → AdvancedVolumes → нажать Add Volume:

ПолеЗначение
Mount TypeVolume Mount
Volume Namegoloot_static
Mount Path/usr/share/nginx/html
Shared Volume

Backend записывает файлы в /static/ (изображения, аватары, баннеры). Static Nginx читает их из /usr/share/nginx/html/. Это один и тот же Docker volume goloot_static. Настройка Backend volume — в секции Backend → Volumes.

# Создать volume (если не создан автоматически)
docker volume create goloot_static

Watch Paths

static-nginx/**

8. Сервис: Docs Site

Настройки в Dokploy

ПараметрЗначениеГде в UI
Namedocs-siteGeneral → Provider
Build Path/General → Provider
Docker Filedocs-site/DockerfileGeneral → Build Type
Docker Context Path.General → Build Type
Port80Domains
AutodeployВЫКЛЮЧЕНGeneral → Deploy Settings
Auto Deploy ВЫКЛЮЧЕН

Docs Site деплоится через GitHub Actions → webhook, а не по push в main. Причина: сборка Docusaurus требует ~4 GB RAM и может упасть на VPS. Сборка происходит на GitHub Actions runners.

Domain

HostPortHTTPS
docs.goloot.online80Let's Encrypt

Branch

ПараметрЗначение
Branchdocs-build (не main!)
Branch = docs-build

Docs Site деплоится из ветки docs-build, куда GitHub Actions пушит собранные статические файлы. Это не main.

Webhook

После создания сервиса скопируй Deploy Webhook URL из Dokploy и добавь в GitHub:

  • Repository → Settings → Secrets → DOKPLOY_DOCS_WEBHOOK

Подробнее: CI/CD Setup


9. Проверка после деплоя

Health Checks

# Backend
curl -s https://api.goloot.online/health
# {"status":"ok","timestamp":"..."}

# Frontend
curl -s -o /dev/null -w "%{http_code}" https://goloot.online
# 200

# Admin
curl -s -o /dev/null -w "%{http_code}" https://admin.goloot.online
# 200

# Redirect Service
curl -s -o /dev/null -w "%{http_code}" https://start.goloot.online
# 200 (основной домен)
curl -s -o /dev/null -w "%{http_code}" https://r.goloot.online
# 200 (push tracking домен)

# Static
curl -s https://static.goloot.online/health
# healthy

# Docs
curl -s -o /dev/null -w "%{http_code}" https://docs.goloot.online
# 200

Логи

В Dokploy UI → Service → Logs — проверь что нет ошибок при старте.

Или через CLI:

# Логи backend (последние 50 строк)
docker logs $(docker ps -q -f name=backend) --tail 50

# Список всех контейнеров
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'

10. Telegram Bot Webhook

После деплоя Backend нужно зарегистрировать webhook, чтобы Telegram отправлял обновления бота на наш сервер.

Когда нужно регистрировать

  • Первый деплой с новым ботом
  • Смена домена или URL webhook
  • Смена TELEGRAM_WEBHOOK_SECRET
  • После удаления webhook (например, при отладке через polling)

Регистрация

curl -X POST "https://api.telegram.org/bot<BOT_TOKEN>/setWebhook" \
-H "Content-Type: application/json" \
-d '{
"url": "https://api.goloot.online/api/telegram/webhook",
"secret_token": "<TELEGRAM_WEBHOOK_SECRET>"
}'
  • <BOT_TOKEN> — токен бота из TELEGRAM_BOT_TOKEN (получить у @BotFather)
  • <TELEGRAM_WEBHOOK_SECRET> — секрет из env переменной TELEGRAM_WEBHOOK_SECRET
secret_token

secret_token — защита webhook от поддельных запросов. Telegram присылает его в заголовке X-Telegram-Bot-Api-Secret-Token, backend проверяет совпадение. Без него webhook принимает запросы от кого угодно.

Ожидаемый ответ:

{"ok": true, "result": true, "description": "Webhook was set"}

Проверка статуса

curl -s "https://api.telegram.org/bot<BOT_TOKEN>/getWebhookInfo" | jq .

Важные поля в ответе:

  • url — зарегистрированный URL
  • has_custom_certificate — должно быть false (Let's Encrypt)
  • pending_update_count — сколько обновлений в очереди
  • last_error_message — последняя ошибка (если есть)

Удаление webhook

Если нужно временно переключиться на polling (для локальной отладки):

curl -X POST "https://api.telegram.org/bot<BOT_TOKEN>/deleteWebhook"
caution

После удаления webhook бот перестанет получать обновления на сервере. Не забудь зарегистрировать заново после отладки.


11. Чеклист

  • Backend задеплоен и /health отвечает
  • Миграции применены
  • Seeds загружены
  • Frontend задеплоен и открывается
  • Admin задеплоен и открывается
  • Redirect Service задеплоен
  • Static Nginx задеплоен с shared volume
  • Docs Site задеплоен из ветки docs-build
  • Telegram webhook зарегистрирован
  • Все HTTPS-сертификаты получены