Staging-деплой (dev/demo)
Staging endpoint: https://api.dimetra.saldin.cloud
Зачем вообще staging
Заголовок раздела «Зачем вообще staging»Локально (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-инстанс.
Инфраструктура
Заголовок раздела «Инфраструктура»| Компонент | Что используем | Зачем |
|---|---|---|
| VPS | PS.kz, 4 vCPU / 4 ГБ | хост |
| Оркестрация | Docker Compose | простота |
| Reverse-proxy + SSL | Caddy 2 + Let’s Encrypt | автоматический https |
| DNS | Cloudflare, 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.staging | SECRET, ADMIN_*, DB_*, STORAGE_R2_* | локально + копируется как /opt/dimetra/.env | да |
deploy/.env.secrets | CF_API_TOKEN, CF_ACCOUNT_ID, CF_ZONE_NAME | только на ноуте | нет |
Почему CF-токен не кладём на VPS: он управляет DNS и R2 всего Cloudflare-аккаунта. На работающем стенде он не нужен — только во время деплоя, чтобы создать A-запись и R2 bucket. Держать его на торчащем в интернет VPS — лишний риск.
Как разворачивается (коротко)
Заголовок раздела «Как разворачивается (коротко)»# 1. Runtime-секреты (едут на VPS)cd deploycp .env.example .env.staging # SECRET, пароли, R2 keys
# 2. Deployer-секреты (только на ноуте)cp .env.secrets.example .env.secrets # CF_API_TOKEN, CF_ACCOUNT_ID
# 3. Inventorycp ansible/inventory.example.yml ansible/inventory.yml # IP, SSH
# 4. Прогон плейбука (30–40 сек)cd ansibleansible-galaxy collection install -r requirements.ymlansible-playbook playbook.yml
# 5. Проверкаcurl https://api.dimetra.saldin.cloud/server/ping # → "pong"open https://api.dimetra.saldin.cloud/adminПлейбук делает на голом VPS:
- Таймзона, 2G swap,
ufw(22/80/443), базовые пакеты - Установка Docker CE + compose plugin
- Копирование compose/Caddyfile/
.envв/opt/dimetra/ docker compose up -dи ждёт/server/ping
Повторный деплой (после изменения compose/env/Caddyfile):
ansible-playbook playbook.yml --tags deployПодробнее команды и troubleshooting — в deploy/README.md.
Применение схемы Directus
Заголовок раздела «Применение схемы Directus»Schema apply теперь часть --tags seed (step 2 — см. ниже). При обычном workflow руками ничего делать не надо.
Если изменил backend/snapshots/schema.yaml локально (новое поле, миграция):
cd backendnpm 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) живут отдельно, при пересоздании стенда не теряются.
Bootstrap данных (./deploy.sh --tags seed)
Заголовок раздела «Bootstrap данных (./deploy.sh --tags seed)»После ./deploy.sh (который поднимает infra + контейнеры) Directus отвечает 200, но БД содержит только дефолтные directus_* таблицы. Чтобы наполнить стенд бизнес-схемой, ролями, переводами, presets, flows — отдельный заход:
cd deploy./deploy.sh --tags seed8 шагов в строгом порядке (всё идемпотентно, можно гонять много раз):
- Login admin → access_token. Ансибл сам POST’ит
/auth/loginс ADMIN_EMAIL/ADMIN_PASSWORD изdeploy/.env.staging. - Schema apply — collections, fields, relations, M2M aliases (
docker compose exec directus npx directus schema apply). - 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 ...). - Roles + permissions —
scripts/setup-roles.sh(4 роли: Project Manager, Content Editor, Client, Bot Service; full CRUD matrix per collection). - Russian translations —
backend/scripts/apply-ru-translations.ts(RU labels на коллекции и поля). - Asset presets —
backend/scripts/apply-asset-presets.ts(5 presets: photos-thumb/preview, blog-thumb/medium/hero) +storage_asset_transform=presetssecurity setting. - Branding —
backend/scripts/apply-branding.ts(favicon + logo). - Directus flows —
backend/scripts/apply-flows.ts(auto-discoverbackend/flows/*.json: 5 photo-category-on-* + folder-create/rename/delete).
Где какой шаг бежит
Заголовок раздела «Где какой шаг бежит»| Step | Runs on | Почему |
|---|---|---|
| 1 login | local controller | HTTP API only, no infra access |
| 2 schema apply | VPS via docker exec directus | directus CLI лежит в контейнере |
| 3 UNIQUE index | VPS via docker exec database | нужен psql + прямой доступ к БД |
| 4 setup-roles.sh | local controller | bash + curl + python3, всё HTTP |
| 5 ru-translations | local controller | npx tsx, HTTP only |
| 6 asset-presets | local controller | npx tsx, HTTP only |
| 7 branding | local controller | npx tsx, HTTP only (uploads logo blob) |
| 8 flows | local controller | npx tsx, HTTP только. Auto-discover из backend/flows/*.json |
Pre-requisites на local controller (твоя машина)
Заголовок раздела «Pre-requisites на local controller (твоя машина)»- 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.
Мобильное приложение (staging-сборка)
Заголовок раздела «Мобильное приложение (staging-сборка)»Мобилка читает EXPO_PUBLIC_DIRECTUS_URL в mobile/services/directus.ts, fallback — http://localhost:8055.
Локальный dev-клиент против staging-бэкенда:
cd mobilecp .env.staging.example .env.local # EXPO_PUBLIC_DIRECTUS_URL=https://api.dimetra.saldin.cloudnpx expo run:iosEAS билды (mobile/eas.json):
| Профиль | Для чего | Backend URL |
|---|---|---|
development | dev-client с localhost | http://localhost:8055 |
staging-simulator | iOS Simulator билд | staging |
staging | APK / ad-hoc на устройство | staging |
production | будущий релиз в сторы | staging (пока тоже) |
cd mobileeas build --platform android --profile staging # APK по ссылкеeas build --platform ios --profile staging-simulator # iOS Simulator билдЧто НЕ покрыто на staging
Заголовок раздела «Что НЕ покрыто на staging»Чтобы потом не было сюрпризов на проде — вот чего на этом стенде нет:
- Бэкапов Postgres. Если БД упадёт — пересоздаём и применяем схему заново, данные теряем.
- Мониторинга / алертов. Лежит — никто не узнает, пока не откроешь.
- CI/CD. Деплой вручную с ноута, нет webhook’ов из git. Зато
--tags seedтеперь воспроизводимый — послеgit pullre-run подтянет новые flows / permissions / translations. - Rate limiting. Caddy пропускает всё подряд.
- Отдельного юзера под БД для Directus. Admin credentials общие.
- Secrets management (Vault/SOPS). Секреты живут в
.env.stagingна ноуте. - Staging-копии R2 данных. Bucket
dimetra-stagingшарится всей командой.
Всё это — задачи для production-плейбука, который будет делаться отдельно, когда подойдёт время реального запуска.
Troubleshooting
Заголовок раздела «Troubleshooting»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/dimetradocker compose down -v # сносит volumes, БД чистаяcd -./deploy/deploy.sh # infra back up./deploy/deploy.sh --tags seed # roles + perms + flows + presets back inSeed 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/dimetradocker 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 backendDIRECTUS_URL=https://api.dimetra.saldin.cloud \ DIRECTUS_TOKEN=<admin-jwt> \ npx tsx scripts/repair-roles.tsIdempotent — повторный run на чистом стейте = noop, удалять ничего не будет.
Docs-сайт (эта страница)
Заголовок раздела «Docs-сайт (эта страница)»Собирается отдельно, деплоится на Cloudflare Pages. Из корня репо:
bash scripts/deploy-docs.sh # build + deploybash scripts/deploy-docs.sh --skip-build # redeploy существующий dist/Первый раз — npx wrangler login для авторизации в Cloudflare.