# 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 в `` → `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..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= 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; нужен только повторный логин в админку.