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

Staging-деплой (dev/demo)

Staging endpoint: https://api.dimetra.saldin.cloud

Локально (docker compose up) всё отлично работает, но:

  • В мобильный билд (EAS APK, TestFlight) нельзя вшить localhost — устройство клиента не достучится.
  • Демо заказчику не показать, пока всё крутится у разработчика на ноуте.
  • На общем бэкенде удобнее отлаживать Flows, Telegram-бот, real-time подписки.

Поэтому поднимаем одну дешёвую копию бэкенда на VPS, с https и доменом, на которую смотрят preview-сборки мобилки.

iOS/Android (EAS staging build)
│ https://api.dimetra.saldin.cloud
┌───────┐ 80/443 ┌─────────────┐
│ Caddy │ ────────▶ │ Directus │──▶ Postgres
│ LE │ │ :8055 │──▶ Redis
└───────┘ └─────────────┘──▶ Cloudflare R2 (files)

Один VPS, всё в docker compose, наружу открыт только Caddy. Бэкапов нет, HA нет, мониторинга нет — это dev-инстанс.

КомпонентЧто используемЗачем
VPSPS.kz, 4 vCPU / 4 ГБхост
ОркестрацияDocker Composeпростота
Reverse-proxy + SSLCaddy 2 + Let’s Encryptавтоматический https
DNSCloudflare, saldin.cloud (личный staging-домен)быстро выдать субдомен
ФайлыCloudflare R2, bucket dimetra-stagingотдельно от dev и будущего prod
ДеплойAnsible playbook (deploy/ansible/)воспроизводимо, пересоздать с нуля
deploy/
├── README.md # пошаговая инструкция (staging only)
├── docker-compose.prod.yml # Postgres + Redis + Directus + Caddy
├── Caddyfile # reverse-proxy, SSL, WS
├── .env.example # шаблон runtime-секретов (→ на VPS)
├── .env.secrets.example # шаблон deployer-секретов (только на ноуте)
└── ansible/
├── ansible.cfg
├── inventory.example.yml # IP/SSH впиши сам
├── requirements.yml
└── playbook.yml # bootstrap + deploy

Реальные секреты (.env.staging, .env.secrets, inventory.yml) — в .gitignore.

У секретов разный blast radius и разный жизненный цикл:

ФайлЧто внутриГде живётНа VPS?
deploy/.env.stagingSECRET, ADMIN_*, DB_*, STORAGE_R2_*локально + копируется как /opt/dimetra/.envда
deploy/.env.secretsCF_API_TOKEN, CF_ACCOUNT_ID, CF_ZONE_NAMEтолько на ноутенет

Почему CF-токен не кладём на VPS: он управляет DNS и R2 всего Cloudflare-аккаунта. На работающем стенде он не нужен — только во время деплоя, чтобы создать A-запись и R2 bucket. Держать его на торчащем в интернет VPS — лишний риск.

Окно терминала
# 1. Runtime-секреты (едут на VPS)
cd deploy
cp .env.example .env.staging # SECRET, пароли, R2 keys
# 2. Deployer-секреты (только на ноуте)
cp .env.secrets.example .env.secrets # CF_API_TOKEN, CF_ACCOUNT_ID
# 3. Inventory
cp ansible/inventory.example.yml ansible/inventory.yml # IP, SSH
# 4. Прогон плейбука (30–40 сек)
cd ansible
ansible-galaxy collection install -r requirements.yml
ansible-playbook playbook.yml
# 5. Проверка
curl https://api.dimetra.saldin.cloud/server/ping # → "pong"
open https://api.dimetra.saldin.cloud/admin

Плейбук делает на голом VPS:

  1. Таймзона, 2G swap, ufw (22/80/443), базовые пакеты
  2. Установка Docker CE + compose plugin
  3. Копирование compose/Caddyfile/.env в /opt/dimetra/
  4. docker compose up -d и ждёт /server/ping

Повторный деплой (после изменения compose/env/Caddyfile):

Окно терминала
ansible-playbook playbook.yml --tags deploy

Подробнее команды и troubleshooting — в deploy/README.md.

Schema apply теперь часть --tags seed (step 2 — см. ниже). При обычном workflow руками ничего делать не надо.

Если изменил backend/snapshots/schema.yaml локально (новое поле, миграция):

Окно терминала
cd backend
npm run schema:snapshot # пересобрать снапшот из локального dev-инстанса
cd ../deploy
./deploy.sh --tags deploy,seed # re-copy snapshot на VPS + re-apply

Снапшот всегда копируется в /opt/dimetra/snapshots/schema.yaml (через --tags deploy), но применяется только в seed (через docker compose exec directus npx directus schema apply). Это разделение даёт момент проверки руками, если нужно — между deploy и seed можно ssh ... 'docker compose exec directus npx directus schema diff ...'.

Если нужно полностью сбросить staging — docker compose down -v на VPS и заново ./deploy.sh && ./deploy.sh --tags seed. Данные в R2 (фото/PDF) живут отдельно, при пересоздании стенда не теряются.

После ./deploy.sh (который поднимает infra + контейнеры) Directus отвечает 200, но БД содержит только дефолтные directus_* таблицы. Чтобы наполнить стенд бизнес-схемой, ролями, переводами, presets, flows — отдельный заход:

Окно терминала
cd deploy
./deploy.sh --tags seed

8 шагов в строгом порядке (всё идемпотентно, можно гонять много раз):

  1. Login admin → access_token. Ансибл сам POST’ит /auth/login с ADMIN_EMAIL/ADMIN_PASSWORD из deploy/.env.staging.
  2. Schema apply — collections, fields, relations, M2M aliases (docker compose exec directus npx directus schema apply).
  3. UNIQUE composite index uq_photo_categories_triplet — Phase 04.5 invariant для photo-category find-or-create flow (docker compose exec database psql ... CREATE UNIQUE INDEX IF NOT EXISTS ...).
  4. Roles + permissionsscripts/setup-roles.sh (4 роли: Project Manager, Content Editor, Client, Bot Service; full CRUD matrix per collection).
  5. Russian translationsbackend/scripts/apply-ru-translations.ts (RU labels на коллекции и поля).
  6. Asset presetsbackend/scripts/apply-asset-presets.ts (5 presets: photos-thumb/preview, blog-thumb/medium/hero) + storage_asset_transform=presets security setting.
  7. Brandingbackend/scripts/apply-branding.ts (favicon + logo).
  8. Directus flowsbackend/scripts/apply-flows.ts (auto-discover backend/flows/*.json: 5 photo-category-on-* + folder-create/rename/delete).
StepRuns onПочему
1 loginlocal controllerHTTP API only, no infra access
2 schema applyVPS via docker exec directusdirectus CLI лежит в контейнере
3 UNIQUE indexVPS via docker exec databaseнужен psql + прямой доступ к БД
4 setup-roles.shlocal controllerbash + curl + python3, всё HTTP
5 ru-translationslocal controllernpx tsx, HTTP only
6 asset-presetslocal controllernpx tsx, HTTP only
7 brandinglocal controllernpx tsx, HTTP only (uploads logo blob)
8 flowslocal controllernpx tsx, HTTP только. Auto-discover из backend/flows/*.json
  • node 20+ (для npx tsx)
  • bash + curl + python3 (для setup-roles.sh)
  • ansible 2.14+ + collections: community.docker, community.general (ansible-galaxy collection install -r requirements.yml)

tsx не требует глобальной установки — подсасывается через npx из backend/devDependencies.

Мобилка читает EXPO_PUBLIC_DIRECTUS_URL в mobile/services/directus.ts, fallback — http://localhost:8055.

Локальный dev-клиент против staging-бэкенда:

Окно терминала
cd mobile
cp .env.staging.example .env.local # EXPO_PUBLIC_DIRECTUS_URL=https://api.dimetra.saldin.cloud
npx expo run:ios

EAS билды (mobile/eas.json):

ПрофильДля чегоBackend URL
developmentdev-client с localhosthttp://localhost:8055
staging-simulatoriOS Simulator билдstaging
stagingAPK / ad-hoc на устройствоstaging
productionбудущий релиз в сторыstaging (пока тоже)
Окно терминала
cd mobile
eas build --platform android --profile staging # APK по ссылке
eas build --platform ios --profile staging-simulator # iOS Simulator билд

Чтобы потом не было сюрпризов на проде — вот чего на этом стенде нет:

  • Бэкапов Postgres. Если БД упадёт — пересоздаём и применяем схему заново, данные теряем.
  • Мониторинга / алертов. Лежит — никто не узнает, пока не откроешь.
  • CI/CD. Деплой вручную с ноута, нет webhook’ов из git. Зато --tags seed теперь воспроизводимый — после git pull re-run подтянет новые flows / permissions / translations.
  • Rate limiting. Caddy пропускает всё подряд.
  • Отдельного юзера под БД для Directus. Admin credentials общие.
  • Secrets management (Vault/SOPS). Секреты живут в .env.staging на ноуте.
  • Staging-копии R2 данных. Bucket dimetra-staging шарится всей командой.

Всё это — задачи для production-плейбука, который будет делаться отдельно, когда подойдёт время реального запуска.

Caddy не выдал сертификат. Открой docker logs dimetra-caddy-1. Обычно это:

  • Cloudflare proxy включён (orange cloud) → выключи, должно быть DNS only
  • A-запись ещё не пропагировалась → dig api.dimetra.saldin.cloud, подожди
  • 80/443 закрыт фаерволом → ufw status

Directus не стартует. docker compose logs -f directus — обычно короткий SECRET, битые R2 credentials или Postgres не успел. Последнее само пройдёт через retry.

Полный reset стенда:

Окно терминала
ssh root@<IP>
cd /opt/dimetra
docker compose down -v # сносит volumes, БД чистая
cd -
./deploy/deploy.sh # infra back up
./deploy/deploy.sh --tags seed # roles + perms + flows + presets back in

Seed login падает с 401 → admin credentials в deploy/.env.staging рассинхронизированы с running container (либо .env поменяли но Directus не перезапустили). Лекарство: ssh ... 'cd /opt/dimetra && docker compose up -d directus' (re-инжектит env), потом ./deploy.sh --tags seed.

Schema apply падает с conflict на существующей БД с данными → сделать diff сначала, посмотреть что хочет менять:

Окно терминала
ssh root@<IP>
cd /opt/dimetra
docker compose exec directus npx directus schema diff /directus/snapshots/schema.yaml

Конфликты обычно — кто-то добавил поле через Admin UI которое не попало в snapshot. Решения: (a) удалить вручную через Admin UI, (b) применить через --force (потеряются Admin UI-changes), (c) пересобрать snapshot из работающего инстанса (schema:snapshot) и перекатить.

setup-roles.sh создал дубликаты ролей/permissions (если старая неидемпотентная версия гонялась раньше или в Admin UI делали ручные правки) → дедуп через repair-roles.ts:

Окно терминала
cd backend
DIRECTUS_URL=https://api.dimetra.saldin.cloud \
DIRECTUS_TOKEN=<admin-jwt> \
npx tsx scripts/repair-roles.ts

Idempotent — повторный run на чистом стейте = noop, удалять ничего не будет.

Собирается отдельно, деплоится на Cloudflare Pages. Из корня репо:

Окно терминала
bash scripts/deploy-docs.sh # build + deploy
bash scripts/deploy-docs.sh --skip-build # redeploy существующий dist/

Первый раз — npx wrangler login для авторизации в Cloudflare.