- /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>
16 KiB
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-сервер.
- Cloudflare → Add a site
shumiland.com.ua(план Free). Cloudflare выдаст пару своих NS (xxx.ns.cloudflare.com/yyy.ns.cloudflare.com). - nic.ua → Сервери імен (NS) → «Список своїх серверів імен» → Змінити: удалить
dns1/dns2.hostiq.ua, вписать две NS Cloudflare. Дождаться статуса Active в Cloudflare. - Записи в Cloudflare (DNS → Records):
A @ → 147.135.209.100— Proxied (оранжевое)A www → 147.135.209.100— Proxied (оранжевое)A cms → 147.135.209.100— DNS only (серое) ← админка/загрузки в обход прокси
- SSL/TLS → Overview → mode = Full (strict).
- SSL/TLS → Origin Server → Create Certificate (покрыть
shumiland.com.ua+*.shumiland.com.ua). Сохранить cert + key → на VPS в./certs/origin.pemи./certs/origin-key.pem(шаг C.3). - Rules → Cache Rules: bypass cache для
/admin*и/api/*(иначе админка/формы поедут). Статику CF кэширует — это плюс. - Rules → Redirect Rules (опционально):
www.shumiland.com.ua→shumiland.com.ua(301). - Проверить:
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). DNScmsдолжен резолвиться на 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.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: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— удалить remotePatternshumi.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:
- Обновить файлы на VPS (git pull в
/opt/shumiland, либо scp Caddyfile + docker-compose.prod.yml). - Отредактировать
/opt/shumiland/.env.production:NEXT_PUBLIC_SITE_URL=https://shumiland.com.uaRESEND_FROM=noreply@shumiland.com.uaRESEND_API_KEY=<боевой ключ Resend>(см. Часть E)MANAGER_EMAILS=<кому слать лиды, через запятую>CERTBOT_DOMAIN=shumiland.com.ua(если ещё используется скриптами)
- Положить Cloudflare Origin Cert в
/opt/shumiland/certs/origin.pem+origin-key.pem(из A.5). - Применить (после того как
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 для cmsappподхватит новыйNEXT_PUBLIC_SITE_URLиз.env.productionпри рестарте. Свежий образ с client-side URL придёт после push вmain(CI build → pull). Можно дождаться зелёного workflow до шага 4.
Часть D — Verification (end-to-end)
dig +short shumiland.com.ua→ адреса Cloudflare;dig +short cms.shumiland.com.ua→147.135.209.100.curl -I https://shumiland.com.ua→200, валидный cert (без-k). В заголовкахserver: cloudflare/cf-ray.curl -I https://cms.shumiland.com.ua→200, валидный Let's Encrypt cert, безcf-ray(напрямую на origin).curl -I https://www.shumiland.com.ua→ редирект на apex.- Браузер
https://shumiland.com.ua— главная грузится, картинки (/api/media/...) отдаются;/adminи/api/*не закэшированы (cf-cache-status: BYPASS/DYNAMIC). https://cms.shumiland.com.ua/admin— залогиниться заново (сессии host-only). Загрузить видео >100 MB → проходит.- SEO canonical в
<head>→shumiland.com.ua(payload.config.ts:119). - Тестовый лид через форму → письмо с
noreply@shumiland.com.ua, в заголовках SPF/DKIM = pass. shumi.ai-impress.comбольше не отдаёт сайт.
Часть E — Resend (верификация домена + почта)
- Resend dashboard → Domains → Add Domain →
shumiland.com.ua. Выбрать регион (EU ближе к UA). - 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;.
- MX на
- В Resend нажать Verify — дождаться, пока все записи зелёные (Verified).
- API key: создать/взять production-ключ Resend → в
.env.productionкакRESEND_API_KEY. RESEND_FROM=noreply@shumiland.com.ua(адрес обязан быть на верифицированном домене).MANAGER_EMAILS— получатели уведомлений о лидах.- Код:
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(Caddytrusted_proxiescloudflare + чтение в 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; нужен только повторный логин в админку.