- /payments now serves the main ticket catalog (was /kvytky) - /checkout serves the legacy single-tariff checkout form - All /kvytky references updated across components, layout, lib, seed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
204 lines
16 KiB
Markdown
204 lines
16 KiB
Markdown
# Runbook: миграция на боевой домен shumiland.com.ua
|
||
|
||
> **Триггер для новой сессии:** когда пользователь скажет «мигрируем на shumiland.com.ua» (или похожее) — прочитать этот файл и выполнять по шагам A → B → C → D. Реализация ещё НЕ начата; ждём подтверждение заказчика.
|
||
|
||
## Context
|
||
|
||
Сайт работает на staging `https://shumi.ai-impress.com/` (VPS `147.135.209.100`, Docker Compose: Postgres + Next.js standalone + Caddy). Caddy сейчас отдаёт **self-signed cert для голого IP** (`auto_https off`). Переключаем продакшн на `shumiland.com.ua`.
|
||
|
||
**Подтверждённые решения:**
|
||
|
||
- **DNS** — переезжаем с hostiq.ua на **Cloudflare** (Free). Свой авторитативный NS не поднимаем.
|
||
- **SSL** — гибрид: публичный домен через прокси Cloudflare (Origin Cert), админка-сабдомен напрямую (Let's Encrypt).
|
||
- **Старый домен** — полный переход, `shumi.ai-impress.com` больше не обслуживается.
|
||
- **Resend** — верифицируем домен сейчас, письма с `noreply@shumiland.com.ua`.
|
||
|
||
**Почему гибрид (важно):** в админку грузят видео **>100 MB**, а Cloudflare Free режет тело запроса на 100 MB (не поднять без Enterprise). Поэтому публичный трафик — через прокси, а загрузки/админка — в обход прокси через DNS-only сабдомен.
|
||
|
||
| Хост | Cloudflare | Сертификат | Назначение |
|
||
| ------------------------- | ------------------- | ------------------------------ | ------------------------------------- |
|
||
| `shumiland.com.ua`, `www` | Proxied (оранжевое) | CF Origin Cert + Universal SSL | Публичный сайт: CDN, DDoS, скрытый IP |
|
||
| `cms.shumiland.com.ua` | DNS only (серое) | Let's Encrypt (Caddy) | Админка + загрузки без лимита 100 MB |
|
||
|
||
**Ключевой нюанс `NEXT_PUBLIC_SITE_URL`:** используется в двух режимах:
|
||
|
||
- _Server-side (runtime)_ — `payload.config.ts` (serverURL, cors, livePreview), `src/middleware.ts`. Берётся из `.env.production` при запуске контейнера. **Главный рычаг.**
|
||
- _Client-side (build-time)_ — `src/components/cms/RefreshRouteOnSave.tsx` (live preview). Запекается при сборке образа. CI (`deploy.yml`) сейчас build-arg НЕ передаёт → значение пустое. Влияние минимальное, но чиним (B.3).
|
||
|
||
---
|
||
|
||
## Часть A — DNS: переезд с hostiq на Cloudflare
|
||
|
||
Домен делегирован на `dns1/dns2.hostiq.ua`. «Свої сервери імен» в nic.ua = поле для NS провайдера, НЕ свой DNS-сервер.
|
||
|
||
1. **Cloudflare → Add a site** `shumiland.com.ua` (план Free). Cloudflare выдаст **пару своих NS** (`xxx.ns.cloudflare.com` / `yyy.ns.cloudflare.com`).
|
||
2. **nic.ua → Сервери імен (NS) → «Список своїх серверів імен» → Змінити**: удалить `dns1/dns2.hostiq.ua`, вписать **две NS Cloudflare**. Дождаться статуса **Active** в Cloudflare.
|
||
3. **Записи в Cloudflare** (DNS → Records):
|
||
- `A @ → 147.135.209.100` — **Proxied (оранжевое)**
|
||
- `A www → 147.135.209.100` — **Proxied (оранжевое)**
|
||
- `A cms → 147.135.209.100` — **DNS only (серое)** ← админка/загрузки в обход прокси
|
||
4. **SSL/TLS → Overview → mode = Full (strict)**.
|
||
5. **SSL/TLS → Origin Server → Create Certificate** (покрыть `shumiland.com.ua` + `*.shumiland.com.ua`). Сохранить cert + key → на VPS в `./certs/origin.pem` и `./certs/origin-key.pem` (шаг C.3).
|
||
6. **Rules → Cache Rules**: bypass cache для `/admin*` и `/api/*` (иначе админка/формы поедут). Статику CF кэширует — это плюс.
|
||
7. **Rules → Redirect Rules** (опционально): `www.shumiland.com.ua` → `shumiland.com.ua` (301).
|
||
8. Проверить: `dig +short shumiland.com.ua` → CF-адреса (проксировано); `dig +short cms.shumiland.com.ua` → `147.135.209.100`.
|
||
|
||
> ⚠️ `cms` держать **DNS only** — иначе (а) упрёмся в лимит 100 MB, (б) Let's Encrypt на Caddy не выпустит cert через прокси (HTTP-01 на порту 80). DNS `cms` должен резолвиться на VPS ДО рестарта Caddy (C.4).
|
||
|
||
---
|
||
|
||
## Часть B — Изменения в репозитории (commit + push в `main`)
|
||
|
||
### B.1. `Caddyfile` — публичный домен на Origin Cert, `cms` на Let's Encrypt
|
||
|
||
Заменить весь файл:
|
||
|
||
```caddyfile
|
||
{
|
||
email admin@shumiland.com.ua
|
||
}
|
||
|
||
# Публичный сайт — за прокси Cloudflare, Origin Certificate
|
||
shumiland.com.ua, www.shumiland.com.ua {
|
||
tls /certs/origin.pem /certs/origin-key.pem
|
||
reverse_proxy app:3000
|
||
}
|
||
|
||
# Админка/загрузки — DNS-only, обход лимита 100 MB, реальный Let's Encrypt cert
|
||
cms.shumiland.com.ua {
|
||
reverse_proxy app:3000
|
||
}
|
||
```
|
||
|
||
Глобальный `auto_https off` **не** ставим: блок с явным `tls` сам отключает авто-выпуск для своих имён, а `cms` без `tls` → Caddy выпустит Let's Encrypt.
|
||
|
||
### B.2. `docker-compose.prod.yml`
|
||
|
||
- `app.build.args.NEXT_PUBLIC_SITE_URL`: `https://147.135.209.100` → `https://shumiland.com.ua` (строка 39).
|
||
- `app`: удалить `environment.NODE_EXTRA_CA_CERTS` (строка 43) и mount `./certs:/certs:ro` (строка 51) — self-signed обвязка больше не нужна.
|
||
- Сервис `caddy`: **оставить** mount `./certs:/certs:ro` (там Origin Cert); добавить персистентные тома для Let's Encrypt-cert'а `cms`:
|
||
```yaml
|
||
volumes:
|
||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||
- ./certs:/certs:ro
|
||
- caddy_data:/data
|
||
- caddy_config:/config
|
||
```
|
||
- В блок `volumes:` добавить `caddy_data:` и `caddy_config:`.
|
||
|
||
### B.3. `.github/workflows/deploy.yml` — запекать корректный client-side URL
|
||
|
||
В шаг `Build and push` (после `cache-to`) добавить:
|
||
|
||
```yaml
|
||
build-args: |
|
||
NEXT_PUBLIC_SITE_URL=https://shumiland.com.ua
|
||
```
|
||
|
||
### B.4. `payload.config.ts` — добавить `cms`-хост в cors/csrf
|
||
|
||
Админка на `cms.shumiland.com.ua`, serverURL = публичный домен → разрешить cms-origin, иначе логин/запросы в админке упрутся в CSRF.
|
||
|
||
- Строка 207 `cors: [siteURL]` → `cors: [siteURL, 'https://cms.shumiland.com.ua']`.
|
||
- Добавить рядом `csrf: [siteURL, 'https://cms.shumiland.com.ua']` (сейчас csrf не задан — по умолчанию = serverURL).
|
||
|
||
### B.5. Чистка хардкодов старого домена / рассинхрона TLD
|
||
|
||
- `next.config.ts:14` — удалить remotePattern `shumi.ai-impress.com` (строка 13 уже содержит `shumiland.com.ua`).
|
||
- Привести fallback-дефолты `shumiland.ua` → `shumiland.com.ua` (срабатывают только если env пуст, но выравниваем):
|
||
- `src/lib/resend.ts:12` (`noreply@shumiland.ua`)
|
||
- `src/seed.ts:212` (`https://shumiland.ua`), `:28`/`:35` (`admin@shumiland.ua`)
|
||
- `src/app/api/admin/seed/route.ts:83`,`:90` (`admin@shumiland.ua`)
|
||
- ожидания в `tests/unit/lib/resend.test.ts`, `tests/unit/hooks/revalidatePath.test.ts`
|
||
|
||
> Не трогать: `.claude/worktrees/...` (дубликат-воркпрейс), `.claude/settings.local.json` (allowlist разрешений Claude, не runtime).
|
||
|
||
---
|
||
|
||
## Часть C — Действия на VPS (`/opt/shumiland`)
|
||
|
||
Конфиги Caddyfile / docker-compose.prod.yml / .env.production на VPS **не синхронизируются CI** (CI делает только `docker compose pull app` + `up app`). После push:
|
||
|
||
1. Обновить файлы на VPS (git pull в `/opt/shumiland`, либо scp Caddyfile + docker-compose.prod.yml).
|
||
2. Отредактировать `/opt/shumiland/.env.production`:
|
||
- `NEXT_PUBLIC_SITE_URL=https://shumiland.com.ua`
|
||
- `RESEND_FROM=noreply@shumiland.com.ua`
|
||
- `RESEND_API_KEY=<боевой ключ Resend>` (см. Часть E)
|
||
- `MANAGER_EMAILS=<кому слать лиды, через запятую>`
|
||
- `CERTBOT_DOMAIN=shumiland.com.ua` (если ещё используется скриптами)
|
||
3. Положить **Cloudflare Origin Cert** в `/opt/shumiland/certs/origin.pem` + `origin-key.pem` (из A.5).
|
||
4. Применить (после того как `cms.shumiland.com.ua` уже резолвится на `147.135.209.100`):
|
||
```bash
|
||
cd /opt/shumiland
|
||
docker compose -f docker-compose.prod.yml up -d caddy app
|
||
docker compose -f docker-compose.prod.yml logs -f caddy # дождаться выдачи Let's Encrypt cert для cms
|
||
```
|
||
`app` подхватит новый `NEXT_PUBLIC_SITE_URL` из `.env.production` при рестарте. Свежий образ с client-side URL придёт после push в `main` (CI build → pull). Можно дождаться зелёного workflow до шага 4.
|
||
|
||
---
|
||
|
||
## Часть D — Verification (end-to-end)
|
||
|
||
1. `dig +short shumiland.com.ua` → адреса Cloudflare; `dig +short cms.shumiland.com.ua` → `147.135.209.100`.
|
||
2. `curl -I https://shumiland.com.ua` → `200`, валидный cert (без `-k`). В заголовках `server: cloudflare` / `cf-ray`.
|
||
3. `curl -I https://cms.shumiland.com.ua` → `200`, валидный Let's Encrypt cert, **без** `cf-ray` (напрямую на origin).
|
||
4. `curl -I https://www.shumiland.com.ua` → редирект на apex.
|
||
5. Браузер `https://shumiland.com.ua` — главная грузится, картинки (`/api/media/...`) отдаются; `/admin` и `/api/*` не закэшированы (`cf-cache-status: BYPASS/DYNAMIC`).
|
||
6. `https://cms.shumiland.com.ua/admin` — залогиниться заново (сессии host-only). **Загрузить видео >100 MB** → проходит.
|
||
7. SEO canonical в `<head>` → `shumiland.com.ua` (`payload.config.ts:119`).
|
||
8. Тестовый лид через форму → письмо с `noreply@shumiland.com.ua`, в заголовках SPF/DKIM = pass.
|
||
9. `shumi.ai-impress.com` больше не отдаёт сайт.
|
||
|
||
---
|
||
|
||
## Часть E — Resend (верификация домена + почта)
|
||
|
||
1. **Resend dashboard → Domains → Add Domain** → `shumiland.com.ua`. Выбрать регион (EU ближе к UA).
|
||
2. Resend выдаст **набор DNS-записей** — добавить их **в Cloudflare** (все как **DNS only / для TXT-MX прокси не применяется**). Точные имена/значения брать из дашборда Resend, не хардкодить. Обычно:
|
||
- **MX** на `send.shumiland.com.ua` → `feedback-smtp.<region>.amazonses.com` (priority 10) — обработка bounce.
|
||
- **TXT SPF** на `send.shumiland.com.ua` → `v=spf1 include:amazonses.com ~all`.
|
||
- **TXT DKIM** на `resend._domainkey.shumiland.com.ua` → выданный публичный ключ.
|
||
- (опц.) **TXT DMARC** на `_dmarc.shumiland.com.ua` → `v=DMARC1; p=none;`.
|
||
3. В Resend нажать **Verify** — дождаться, пока все записи зелёные (Verified).
|
||
4. **API key**: создать/взять production-ключ Resend → в `.env.production` как `RESEND_API_KEY`.
|
||
5. `RESEND_FROM=noreply@shumiland.com.ua` (адрес обязан быть на верифицированном домене).
|
||
6. `MANAGER_EMAILS` — получатели уведомлений о лидах.
|
||
7. Код: `src/lib/resend.ts` (`sendLeadAlert`) и `payload.config.ts:49` (`defaultFromAddress`) уже читают `RESEND_FROM` с дефолтом на домен — после B.5 дефолты выровнены на `.com.ua`.
|
||
|
||
---
|
||
|
||
## Часть F — Опциональный хардненинг и rollback
|
||
|
||
**Хардненинг (по желанию, после успешного переключения):**
|
||
|
||
- На VPS закрыть firewall'ом порты 80/443 для всех, кроме [IP-диапазонов Cloudflare](https://www.cloudflare.com/ips/) — origin не дёргают в обход прокси. **Осторожно:** `cms` (DNS-only) должен остаться доступен напрямую — не блокировать его трафик.
|
||
- Реальный IP клиента за прокси: если в лид-форме/логах важен IP — настроить доверие заголовку `CF-Connecting-IP` (Caddy `trusted_proxies` cloudflare + чтение в Next.js). Для маркетинг-сайта обычно некритично.
|
||
|
||
**Rollback (если что-то пошло не так):**
|
||
|
||
- DNS: вернуть в nic.ua прежние NS hostiq (`dns1/dns2.hostiq.ua`) — сайт снова резолвится по-старому (с учётом TTL).
|
||
- App: задеплоить предыдущий образ — `TAG=<old-sha> docker compose -f docker-compose.prod.yml up -d app`.
|
||
- `.env.production`: вернуть прежний `NEXT_PUBLIC_SITE_URL`.
|
||
- Caddyfile: вернуть прежнюю версию (self-signed/IP) из git history.
|
||
|
||
---
|
||
|
||
## Сводка файлов к изменению
|
||
|
||
| Файл / место | Изменение |
|
||
| ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
|
||
| `Caddyfile` | Публичный домен на Origin Cert + `cms` на Let's Encrypt |
|
||
| `docker-compose.prod.yml` | build-arg URL; убрать `NODE_EXTRA_CA_CERTS` + certs-mount у `app`; добавить caddy volumes |
|
||
| `payload.config.ts` | `cms.shumiland.com.ua` в `cors` + `csrf` |
|
||
| `.github/workflows/deploy.yml` | `build-args: NEXT_PUBLIC_SITE_URL` |
|
||
| `next.config.ts` | Удалить remotePattern старого домена |
|
||
| `src/lib/resend.ts`, `src/seed.ts`, `src/app/api/admin/seed/route.ts`, тесты | `shumiland.ua` → `shumiland.com.ua` |
|
||
| `/opt/shumiland/.env.production` (VPS) | URL, Resend, manager emails |
|
||
| `/opt/shumiland/certs/origin.pem` + `origin-key.pem` (VPS) | Cloudflare Origin Cert |
|
||
| Cloudflare dashboard | NS, A-записи (apex/www proxied, cms DNS-only), Full strict, Origin Cert, Cache Rules, Redirect Rule |
|
||
| Resend dashboard + Cloudflare DNS | Add domain, MX/SPF/DKIM/DMARC, Verify, API key |
|
||
|
||
## Что НЕ требует изменений
|
||
|
||
- `serverURL` / `cors` (apex) / `livePreview` — выводятся из `NEXT_PUBLIC_SITE_URL` автоматически. Меняем только добавление `cms`-хоста в cors/csrf.
|
||
- Cookie domain — не задан, куки host-only; нужен только повторный логин в админку.
|