Shumiland/docs/domain-migration.md
Vadym Samoilenko 03a0af4080
Some checks are pending
CI / Type Check (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Unit Tests (push) Waiting to run
Deploy / Build & Push Image (push) Waiting to run
Deploy / Deploy to VPS (push) Blocked by required conditions
feat(routing): move /kvytky → /payments, rename checkout → /checkout
- /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>
2026-06-11 10:44:34 +01:00

16 KiB
Raw Blame History

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.100Proxied (оранжевое)
    • A www → 147.135.209.100Proxied (оранжевое)
    • A cms → 147.135.209.100DNS 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.uashumiland.com.ua (301).
  8. Проверить: dig +short shumiland.com.ua → CF-адреса (проксировано); dig +short cms.shumiland.com.ua147.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

Заменить весь файл:

{
    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.100https://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:
    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) добавить:

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.uashumiland.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):
    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.ua147.135.209.100.
  2. curl -I https://shumiland.com.ua200, валидный cert (без -k). В заголовках server: cloudflare / cf-ray.
  3. curl -I https://cms.shumiland.com.ua200, валидный 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 Domainshumiland.com.ua. Выбрать регион (EU ближе к UA).
  2. Resend выдаст набор DNS-записей — добавить их в Cloudflare (все как DNS only / для TXT-MX прокси не применяется). Точные имена/значения брать из дашборда Resend, не хардкодить. Обычно:
    • MX на send.shumiland.com.uafeedback-smtp.<region>.amazonses.com (priority 10) — обработка bounce.
    • TXT SPF на send.shumiland.com.uav=spf1 include:amazonses.com ~all.
    • TXT DKIM на resend._domainkey.shumiland.com.ua → выданный публичный ключ.
    • (опц.) TXT DMARC на _dmarc.shumiland.com.uav=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 — 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.uashumiland.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; нужен только повторный логин в админку.