refactor(caddy): разделить site-блоки по хостам под переезд
Some checks failed
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Failing after 48s
Some checks failed
CI Trade-In / changes (pull_request) Successful in 8s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Failing after 48s
Корневой Caddyfile содержал все восемь доменов. Деплой синхронизирует
рабочее дерево с origin/main и перечитывает конфиг, поэтому на Selectel
приехал бы файл целиком — и Caddy начал бы выпускать сертификаты для
obsidian/errors/git, чей DNS указывает на Beget. ACME падал бы на HTTP-01,
с риском упереться в rate limit Let's Encrypt.
Вторая, более тяжёлая сторона той же проблемы: сейчас ЭТОТ Caddy —
единственный публичный вход и для тех трёх доменов тоже. Он уезжает, а
Forgejo, GlitchTip и CouchDB остаются. Без разделения они отдавали бы
502, оставаясь живыми. Теперь на Beget остаётся Caddy с CADDY_SITES=infra.
caddy/sites/apps.caddy gendsgn.ru, www, meraocenka, merahome, meraotsenka
caddy/sites/infra.caddy obsidian, errors, git
Корневой файл ужался с 522 строк до 96: глобальные опции, snippet'ы,
блок :80 и два import. Site-блоки перенесены дословно, вместе с
комментариями.
Проверено caddy adapt в трёх режимах:
без переменной -> 8 доменов (текущее поведение Beget, не меняется)
CADDY_SITES=apps -> 5
CADDY_SITES=infra -> 3
Две правки, найденные валидацией: синтаксис дефолта у Caddy {$VAR:default},
а не {$VAR:-default}; путь к users.caddy.snippet из подкаталога стал
../users.caddy.snippet — import резолвится относительно импортирующего файла.
Refs #3059, #3057, #2989
This commit is contained in:
parent
ccdd7553ba
commit
dacc8b410c
4 changed files with 563 additions and 441 deletions
456
Caddyfile
456
Caddyfile
|
|
@ -31,450 +31,24 @@
|
||||||
# (тег деградирует в "(none)" — forwarder это уже обрабатывает gracefully, не падает).
|
# (тег деградирует в "(none)" — forwarder это уже обрабатывает gracefully, не падает).
|
||||||
# Событие basic_auth 401 (remote_ip / uri / method) по-прежнему уходит в GlitchTip.
|
# Событие basic_auth 401 (remote_ip / uri / method) по-прежнему уходит в GlitchTip.
|
||||||
|
|
||||||
gendsgn.ru {
|
|
||||||
encode zstd gzip
|
|
||||||
|
|
||||||
log {
|
# ── Site-блоки вынесены по хостам (#3059, переезд 30.08) ────────────────────
|
||||||
output file /var/log/caddy/gendsgn.ru.log {
|
# Раньше все восемь доменов жили прямо здесь. После разделения продуктов между
|
||||||
roll_size 50MiB
|
# двумя хостами это стало опасно: деплой синхронизирует рабочее дерево с
|
||||||
roll_keep 5
|
# origin/main и перечитывает конфиг, поэтому на Selectel приезжал бы файл
|
||||||
roll_keep_for 720h
|
# целиком — и Caddy начинал бы выпускать сертификаты для obsidian/errors/git,
|
||||||
}
|
# чей DNS указывает на Beget. ACME падал бы на HTTP-01, с риском упереться в
|
||||||
format json
|
# rate limit Let's Encrypt.
|
||||||
}
|
|
||||||
|
|
||||||
# Отдельный лог только для auth-событий.
|
|
||||||
# Forwarder (ops/glitchtip-auth-forwarder) читает именно этот файл.
|
|
||||||
# Retention 7 дней (меньше чем main log) — содержит plain Base64 credentials.
|
|
||||||
log auth_audit {
|
|
||||||
output file /var/log/caddy/auth_audit.log {
|
|
||||||
roll_size 10MiB
|
|
||||||
roll_keep 3
|
|
||||||
roll_keep_for 168h
|
|
||||||
}
|
|
||||||
format json
|
|
||||||
}
|
|
||||||
|
|
||||||
route {
|
|
||||||
# /health и /preview/* — public, без auth, short-circuit.
|
|
||||||
handle /health {
|
|
||||||
reverse_proxy backend:8000
|
|
||||||
}
|
|
||||||
|
|
||||||
# Static HTML mockups для review (audit alternatives).
|
|
||||||
# Public access — без auth (по запросу 2026-05-17).
|
|
||||||
handle_path /preview/* {
|
|
||||||
root * /srv/preview
|
|
||||||
file_server browse
|
|
||||||
}
|
|
||||||
|
|
||||||
# Trade-In UI preview — public CI surface (#801). Рендерит mock-фикстуру
|
|
||||||
# «денежного экрана» без бэкенда → axe/lighthouse гоняются без креды.
|
|
||||||
# Реальных клиентских данных нет (статичная фикстура) → безопасно публично.
|
|
||||||
# ДО auth-import: route матчит сверху вниз, handle short-circuit'ит.
|
|
||||||
# Без strip — Next.js basePath=/trade-in ждёт префикс в URL (как @tradein).
|
|
||||||
# ui-preview + его статика (_next/static — CSS/JS бандлы, без секретов).
|
|
||||||
# Оба ДО auth-import, иначе ассеты страницы уходят в @tradein (под auth) → 401 → без CSS.
|
|
||||||
@uipreview path /trade-in/ui-preview/* /trade-in/_next/static/*
|
|
||||||
handle @uipreview {
|
|
||||||
reverse_proxy tradein-frontend:3000 {
|
|
||||||
# #2558 review: тот же периметр-scrub, что и у /trade-in/api/* и
|
|
||||||
# @tradein ниже — этот блок тоже теперь ДО basic_auth, клиент
|
|
||||||
# мог бы прислать свой X-Authenticated-User. Сейчас инертно
|
|
||||||
# (страница статична, у tradein-frontend нет секрета для
|
|
||||||
# X-Internal-Auth-Secret), но убираем ради единообразия периметра,
|
|
||||||
# а не полагаясь на то, что downstream ничего не делает с заголовком.
|
|
||||||
header_up -X-Authenticated-User
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# #2558: Trade-In MVP subproject (tradein-mvp/) — gendesign-tradein docker
|
|
||||||
# stack, подключен через gendesign_shared network. Секция ЦЕЛИКОМ ДО
|
|
||||||
# `import caddy/users.caddy.snippet` ниже — /trade-in имеет собственную
|
|
||||||
# авторизацию (форма входа + opaque session-cookie, #2552; RBAC-проверка
|
|
||||||
# роли внутри tradein-backend, `app/core/rbac.py`), Site Finder basic_auth
|
|
||||||
# ей больше не нужен и не должен применяться (short-circuit сверху вниз,
|
|
||||||
# как /health и /preview/* выше).
|
|
||||||
#
|
|
||||||
# X-Authenticated-User — ЯВНОЕ УДАЛЕНИЕ (`header_up -X-Authenticated-User`),
|
|
||||||
# НЕ `header_up X-Authenticated-User {http.auth.user.id}`. Причина: этот
|
|
||||||
# блок больше не идёт ПОСЛЕ basic_auth, поэтому `{http.auth.user.id}`
|
|
||||||
# никогда не резолвится авторизованным юзером на этом пути.
|
|
||||||
# Проверено эмпирически (echo-стенд на образе caddy:2, `caddy adapt`):
|
|
||||||
# старая Set-форма (`header_up X-Authenticated-User {http.auth.user.id}`)
|
|
||||||
# НЕ пропустила бы клиентский заголовок насквозь и НЕ оставила бы поле
|
|
||||||
# пустым — Caddy подставляет НЕРАЗРЕШЁННЫЙ плейсхолдер как ЛИТЕРАЛЬНУЮ
|
|
||||||
# строку (`ReplaceKnown`), т.е. upstream получил бы буквально
|
|
||||||
# `X-Authenticated-User: {http.auth.user.id}`. Для backend (auth_mode=
|
|
||||||
# "dual", `app/core/config.py`) это НЕ подмена личности — legacy path
|
|
||||||
# (`rbac.py:186`) сделал бы `get_role("{http.auth.user.id}")`, юзер не
|
|
||||||
# найден в roles.yaml → 403 для всех. Т.е. старая форма была бы не
|
|
||||||
# security-дырой, а fail-closed-but-сломанной (все trade-in запросы без
|
|
||||||
# session-cookie получали бы 403 вместо ожидаемого 401/редиректа на логин).
|
|
||||||
# `-Field` остаётся правильным выбором не потому что Set был бы дырой, а
|
|
||||||
# потому что это ЕДИНСТВЕННАЯ форма с явно задокументированной семантикой
|
|
||||||
# "удалить заголовок" (Caddyfile reverse_proxy directive: `-<field>` =
|
|
||||||
# delete) — корректное поведение не должно зависеть от того, как именно
|
|
||||||
# Caddy трактует нерезолвленный/пустой плейсхолдер в Set-операции.
|
|
||||||
# X-Internal-Auth-Secret НЕ трогаем — #2213-секрет всегда перезаписывается
|
|
||||||
# из env (Set-операция с непустым значением, никак не связана с auth-гейтом
|
|
||||||
# basic_auth), это единственное, что теперь отсекает подделку заголовков
|
|
||||||
# изнутри gendesign_shared network для legacy dual-mode пути.
|
|
||||||
handle /trade-in/api/* {
|
|
||||||
# `handle_path /trade-in/api/*` стрипал бы целиком /trade-in/api;
|
|
||||||
# FastAPI router замаунтен на /api/v1/trade-in/* — нужен strip только
|
|
||||||
# префикса basePath /trade-in (Next.js basePath leak).
|
|
||||||
uri strip_prefix /trade-in
|
|
||||||
reverse_proxy tradein-backend:8000 {
|
|
||||||
header_up -X-Authenticated-User
|
|
||||||
header_up X-Internal-Auth-Secret {env.TRADEIN_INTERNAL_AUTH_SECRET}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# gendsgn.ru/sale-share — короткий адрес standalone-продукта «Поиск домов».
|
|
||||||
# Next basePath=/trade-in → редиректим на канонический /trade-in/sale-share
|
|
||||||
# (тот же tradein-frontend контейнер; query-string сохраняется). True vanity-URL
|
|
||||||
# в адресной строке требует отдельного Next-app с basePath=/sale-share.
|
|
||||||
# #2558: перенесён ВЫШЕ auth-import вместе с trade-in — редирект ведёт на
|
|
||||||
# /trade-in/sale-share, для которого теперь нет Caddy basic_auth (как и
|
|
||||||
# для остального /trade-in). Это НЕ делает страницу публичной: она всё
|
|
||||||
# ещё за собственной авторизацией trade-in — `RouteGuard` во фронте
|
|
||||||
# (`app/layout.tsx`) и сессия для `/api/v1/buildings/sale-share*` на
|
|
||||||
# бэке; без валидной сессии юзер получит редирект на /login, а не
|
|
||||||
# контент. Смысл переноса — не открыть страницу всем, а убрать
|
|
||||||
# несогласованность: короткий URL не должен быть строже (Caddy
|
|
||||||
# basic_auth) целевого адреса, к которому и так уже нет
|
|
||||||
# basic_auth-барьера (только собственный login trade-in).
|
|
||||||
#
|
|
||||||
# ОБНОВЛЕНО 2026-07-31: доступ к разделу сузился с «pilot + admin» до
|
|
||||||
# ТОЛЬКО admin — «Поиск домов» признан тестовым продуктом, клиентам не
|
|
||||||
# показывается (deny в auth/roles.yaml для pilot и analyst + в
|
|
||||||
# DB_ROLE_PATHS для employee/manager). Сам редирект не трогаем: он ведёт
|
|
||||||
# на страницу, а гейт стоит на роли — для всех, кроме admin, короткий
|
|
||||||
# адрес приведёт на NoAccessScreen.
|
|
||||||
@saleshare path /sale-share /sale-share/
|
|
||||||
handle @saleshare {
|
|
||||||
redir /trade-in/sale-share permanent
|
|
||||||
}
|
|
||||||
|
|
||||||
# Matcher `path /trade-in /trade-in/*` ловит И /trade-in (без слеша),
|
|
||||||
# И /trade-in/ + /trade-in/anything. Без обоих случаев `handle /trade-in/*`
|
|
||||||
# пропускал /trade-in без слеша → попадал в общий frontend → пустой ответ.
|
|
||||||
@tradein path /trade-in /trade-in/*
|
|
||||||
handle @tradein {
|
|
||||||
# Next.js basePath=/trade-in — фронт сам ждёт префикса в URL
|
|
||||||
reverse_proxy tradein-frontend:3000 {
|
|
||||||
# См. комментарий над /trade-in/api/* выше — та же логика (явное
|
|
||||||
# удаление вместо Set с пустым {http.auth.user.id}).
|
|
||||||
header_up -X-Authenticated-User
|
|
||||||
header_up X-Internal-Auth-Secret {env.TRADEIN_INTERNAL_AUTH_SECRET}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Auth gate — с #2558 применяется ТОЛЬКО к Site Finder (handle /api/* и
|
|
||||||
# handle {} ниже). Trade-In уже отработал и short-circuit'нул выше.
|
|
||||||
import caddy/users.caddy.snippet
|
|
||||||
|
|
||||||
handle /api/* {
|
|
||||||
reverse_proxy backend:8000 {
|
|
||||||
header_up X-Authenticated-User {http.auth.user.id}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handle {
|
|
||||||
reverse_proxy frontend:3000 {
|
|
||||||
header_up X-Authenticated-User {http.auth.user.id}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
www.gendsgn.ru {
|
|
||||||
redir https://gendsgn.ru{uri} permanent
|
|
||||||
}
|
|
||||||
|
|
||||||
# МЕРА B2C — публичный периметр (ЭТАП 1 плана B2C-запуска, БЕЗ функционала).
|
|
||||||
#
|
#
|
||||||
# Архитектурное решение: отдельный домен, а НЕ дырка в блоке gendsgn.ru
|
# caddy/sites/apps.caddy gendsgn.ru, www, meraocenka, merahome, meraotsenka
|
||||||
# выше. На gendsgn.ru модель "запрещено всё, кроме дырок ВЫШЕ auth-import" —
|
# -> уезжают на Selectel
|
||||||
# порядко-зависимая и общая для B2B (trade-in v2, admin, scrapers, /api/*).
|
# caddy/sites/infra.caddy obsidian, errors, git
|
||||||
# Здесь, наоборот, allowlist-by-default: basic_auth НЕТ ВООБЩЕ (не импортируем
|
# -> остаются на Beget (Forgejo, GlitchTip, CouchDB)
|
||||||
# caddy/users.caddy.snippet), потому что на этом site-блоке B2B-маршрутов
|
|
||||||
# физически не объявлено — их нечего "открывать". Явно перечислены РОВНО два
|
|
||||||
# handle (корень "/" + статика Next _next/*), всё остальное — финальный
|
|
||||||
# catch-all `handle { respond 404 }`. Регресс-тест на эту модель:
|
|
||||||
# scripts/smoke-mera-perimeter.sh (проверяет, что B2B-путь здесь = 404, а не
|
|
||||||
# 200/401 — т.е. не был случайно проброшен).
|
|
||||||
#
|
#
|
||||||
# Next.js basePath=/trade-in запечён в prod-образ tradein-frontend (тот же
|
# CADDY_SITES выбирает подмножество. Дефолт `*` = оба файла = ТЕКУЩЕЕ поведение
|
||||||
# контейнер, что обслуживает и gendsgn.ru/trade-in/*, см. build-args в
|
# Beget, где сейчас обслуживаются все восемь доменов — то есть до переезда
|
||||||
# .forgejo/workflows/deploy-tradein.yml) — поэтому корень домена rewrite'ится
|
# ничего не меняется. В окне: на Selectel CADDY_SITES=apps, на Beget=infra.
|
||||||
# на internal-путь /trade-in/mera-public (страница-заглушка,
|
import caddy/sites/{$CADDY_SITES:*}.caddy
|
||||||
# tradein-mvp/frontend/src/app/mera-public/). Пользователь префикс /trade-in
|
|
||||||
# никогда не видит — rewrite меняет путь ТОЛЬКО для Caddy→backend запроса,
|
|
||||||
# это не HTTP-редирект браузера.
|
|
||||||
#
|
|
||||||
# DNS: A-record meraocenka.ru → IP VPS — ТРЕБУЕТСЯ ДО того, как сюда придёт
|
|
||||||
# реальный трафик. Если записи ещё нет на момент деплоя этого блока: `caddy
|
|
||||||
# reload`/`up -d --force-recreate caddy` в deploy.yml НЕ падает (конфиг
|
|
||||||
# синтаксически валиден, ошибка сертификата асинхронна и per-hostname) — Caddy
|
|
||||||
# просто залогирует неудачную попытку ACME-выпуска для meraocenka.ru (DNS не
|
|
||||||
# резолвится на этот сервер → HTTP-01/TLS-ALPN challenge недостижим) и продолжит
|
|
||||||
# ретраить с backoff, ПОКА запись не появится. Остальные site-блоки в этом же
|
|
||||||
# Caddyfile (gendsgn.ru, obsidian.gendsgn.ru и т.д.) не затрагиваются —
|
|
||||||
# автоматический HTTPS в Caddy изолирован per-hostname. Повторные неудачные попытки ДО
|
|
||||||
# появления DNS могут исчерпать rate-limit Let's Encrypt (5 failed
|
|
||||||
# validations/hostname/hour) — не критично, просто подождать; `docker volume
|
|
||||||
# rm gendesign_caddy_data` для этого НЕ нужен (и вообще требует user-approval).
|
|
||||||
meraocenka.ru {
|
|
||||||
encode zstd gzip
|
|
||||||
|
|
||||||
log {
|
|
||||||
output file /var/log/caddy/meraocenka.ru.log
|
|
||||||
}
|
|
||||||
|
|
||||||
# Корень домена → лэндинг МЕРЫ (#2615 заменил заглушку этого этапа на
|
|
||||||
# полноценную страницу). rewrite добавляет basePath-префикс только для
|
|
||||||
# Caddy→backend хопа, пользователь /trade-in никогда не видит.
|
|
||||||
handle / {
|
|
||||||
rewrite * /trade-in/mera-public
|
|
||||||
reverse_proxy tradein-frontend:3000 {
|
|
||||||
# Тот же периметр-скраб, что у @uipreview (:87) и @tradein ниже.
|
|
||||||
# Этот блок вообще не под basic_auth, поэтому анонимный клиент
|
|
||||||
# тем более может прислать свой X-Authenticated-User. Сейчас
|
|
||||||
# инертно (лэндинг статичен, backend-вызовов нет), но снимаем
|
|
||||||
# ради единообразия периметра, а не полагаясь на то, что
|
|
||||||
# downstream ничего не делает с заголовком — иначе на этапе 5,
|
|
||||||
# когда откроется публичный /estimate, это станет дырой.
|
|
||||||
header_up -X-Authenticated-User
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Короткие адреса страниц публичного сайта. Именно они напечатаны ВНУТРИ
|
|
||||||
# юридических документов (оферта ссылается на meraocenka.ru/refund,
|
|
||||||
# политика возврата — на meraocenka.ru/oferta) и уходят в заявку эквайеру,
|
|
||||||
# поэтому обязаны резолвиться сами по себе.
|
|
||||||
#
|
|
||||||
# ЭТО ЕДИНСТВЕННЫЙ ВИД АДРЕСА, КОТОРЫЙ ВИДИТ ЧЕЛОВЕК (решение владельца,
|
|
||||||
# 15.08.2026). Раньше навигация внутри сайта ходила по длинным
|
|
||||||
# /trade-in/mera-public/... — так короткие адреса и длинные существовали
|
|
||||||
# параллельно. Теперь длинные отдают 301 на короткие (см. handle ниже), а
|
|
||||||
# ссылки на страницах эмитятся сразу короткими (см. `PublicLink` во
|
|
||||||
# фронте — обычный <a>, потому что next/link подставляет basePath).
|
|
||||||
#
|
|
||||||
# `rewrite`, а не `redir`: адрес в строке браузера должен остаться коротким
|
|
||||||
# — модератор эквайера открывает ссылку из заявки и видит ровно тот URL,
|
|
||||||
# который в ней указан. Каноничность для поисковиков задана отдельно, через
|
|
||||||
# `alternates.canonical` на каждой странице.
|
|
||||||
#
|
|
||||||
# Пути перечислены поимённо, а не шаблоном: allowlist-by-default этого
|
|
||||||
# site-блока — часть периметра (#2545), и превращать его в «любой корневой
|
|
||||||
# путь проксируется» нельзя. Новая публичная страница = новая строка здесь
|
|
||||||
# (и проверка в scripts/smoke-mera-perimeter.sh).
|
|
||||||
#
|
|
||||||
# NB: корень «/» СЮДА НЕ ВХОДИТ — он выше, отдельным handle. Причина
|
|
||||||
# техническая: здесь цель собирается как `/trade-in/mera-public{path}`, а
|
|
||||||
# для «/» это дало бы `/trade-in/mera-public/` со слэшем на конце. Next при
|
|
||||||
# `trailingSlash: false` ответил бы на такой путь 308-редиректом на вариант
|
|
||||||
# без слэша — то есть на ДЛИННЫЙ адрес, который handle ниже отправит 301 на
|
|
||||||
# «/», и запрос закольцуется.
|
|
||||||
# `/v3` — ВРЕМЕННОЕ превью второго варианта дизайна, а не публичная
|
|
||||||
# страница: владелец сравнивает его с текущим лэндингом. Оно `noindex` и
|
|
||||||
# ни с одной страницы на него нет ссылки. Убрать эту строку в тот момент,
|
|
||||||
# когда вариант выберут и он станет корнем.
|
|
||||||
@meraPages path /estimate /oferta /refund /privacy /v3
|
|
||||||
handle @meraPages {
|
|
||||||
rewrite * /trade-in/mera-public{path}
|
|
||||||
reverse_proxy tradein-frontend:3000 {
|
|
||||||
header_up -X-Authenticated-User
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Тот же адрес со слэшем на конце → 301 на канонический вид без слэша.
|
|
||||||
# Слэш дописывают мессенджеры, автолинкификаторы и сами люди, а матчер
|
|
||||||
# `path` требует точного совпадения — без этой ветки `/oferta/` отдавал бы
|
|
||||||
# голый 404 (так было и до этого PR, с момента #2615). Заодно это
|
|
||||||
# замыкает цепочку для длинных адресов со слэшем: они приходят на короткий
|
|
||||||
# со слэшем и здесь нормализуются.
|
|
||||||
@meraShortSlash path_regexp shortslash ^/(estimate|oferta|refund|privacy|v3)/$
|
|
||||||
handle @meraShortSlash {
|
|
||||||
redir * /{re.shortslash.1} permanent
|
|
||||||
}
|
|
||||||
|
|
||||||
# Длинные адреса поддерева → 301 на короткие. Один канонический адрес у
|
|
||||||
# страницы, а не два работающих.
|
|
||||||
#
|
|
||||||
# Зачем вообще оставлять длинные: они уже разошлись — ими ссылались подвал
|
|
||||||
# и шапка до 15.08.2026, они могли попасть в закладки и в переписку. 301
|
|
||||||
# (а не 404) сохраняет эти ссылки живыми и заодно передаёт поисковикам, что
|
|
||||||
# канонический адрес один.
|
|
||||||
#
|
|
||||||
# ЗДЕСЬ ЖЕ ЧИНИТСЯ БАГ: прежний матчер был `/trade-in/mera-public/*` — со
|
|
||||||
# слэшем и звёздочкой, поэтому ГОЛЫЙ `/trade-in/mera-public` (без хвоста)
|
|
||||||
# под него не подпадал и падал в catch-all 404. Ровно на этот адрес вела
|
|
||||||
# ссылка «Главная» в подвале v3, то есть она была мёртвой (замер на проде
|
|
||||||
# 15.08.2026). Первый матчер ниже ловит обе формы — со слэшем и без.
|
|
||||||
#
|
|
||||||
# `redir * <куда>`, а НЕ `redir <куда>`. Первый аргумент директивы, если он
|
|
||||||
# начинается со слэша, Caddy разбирает как inline path-matcher — то есть
|
|
||||||
# `redir / permanent` означает «для пути / редиректить на permanent», а не
|
|
||||||
# «редиректить на /». Проверено на живом Caddy: без `*` длинные адреса
|
|
||||||
# отдавали пустой 200 (матчер не совпадал, директива не срабатывала, тело
|
|
||||||
# пустое) — хуже, чем 404, потому что выглядит как рабочая пустая страница.
|
|
||||||
@meraLongRoot path /trade-in/mera-public /trade-in/mera-public/
|
|
||||||
handle @meraLongRoot {
|
|
||||||
redir * / permanent
|
|
||||||
}
|
|
||||||
|
|
||||||
# Длинные адреса страниц → короткие. Пути перечислены ПОИМЁННО, обе формы
|
|
||||||
# (со слэшем на конце и без) — не шаблоном и не регекспом.
|
|
||||||
#
|
|
||||||
# ПОЧЕМУ НЕ РЕГЕКСП С ЗАХВАТОМ ХВОСТА. Очевидный вариант
|
|
||||||
# `path_regexp ^/trade-in/mera-public/(.+)$` + `redir /{re.…1}` — открытый
|
|
||||||
# редирект. Захват берётся из РАСКОДИРОВАННОГО пути, поэтому
|
|
||||||
# `/trade-in/mera-public/%5Cevil.example/pay` даёт цель `/\evil.example/pay`,
|
|
||||||
# а браузеры трактуют `/\` как `//` — Location уводит на ЧУЖОЙ хост. Это
|
|
||||||
# готовая фишинговая заготовка с домена, который напечатан внутри оферты и
|
|
||||||
# уходит модератору эквайера. Проверено на живом Caddy, воспроизводится.
|
|
||||||
# С поимённым списком такой путь просто не матчится и падает в 404 ниже.
|
|
||||||
#
|
|
||||||
# ПОЧЕМУ `uri strip_prefix` + `{uri}`, А НЕ `redir /oferta` в каждой ветке.
|
|
||||||
# `{uri}` переносит query-строку: уже размещённые ссылки с UTM-метками
|
|
||||||
# после редиректа не теряют атрибуцию. Обёртка `route` обязательна —
|
|
||||||
# порядок директив внутри `handle` определяет Caddy, и без неё `redir`
|
|
||||||
# выполняется РАНЬШЕ `uri`, отдавая Location, равный исходному адресу
|
|
||||||
# (бесконечный цикл; поймано на локальном стенде).
|
|
||||||
@meraLongPages path /trade-in/mera-public/estimate /trade-in/mera-public/estimate/ /trade-in/mera-public/oferta /trade-in/mera-public/oferta/ /trade-in/mera-public/refund /trade-in/mera-public/refund/ /trade-in/mera-public/privacy /trade-in/mera-public/privacy/ /trade-in/mera-public/v3 /trade-in/mera-public/v3/
|
|
||||||
handle @meraLongPages {
|
|
||||||
route {
|
|
||||||
uri strip_prefix /trade-in/mera-public
|
|
||||||
redir * {uri} permanent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Next.js уже эмитит ссылки на статику с /trade-in-префиксом (тот же
|
|
||||||
# basePath) — passthrough без rewrite. Нужны для рендера страницы (JS/CSS
|
|
||||||
# чанки), сами по себе не содержат ни B2B-данных, ни секретов.
|
|
||||||
#
|
|
||||||
# Именно `static/*`, а не весь `_next/*` — тот же матчер, что у @uipreview
|
|
||||||
# (:78), который в проде доказал, что этого хватает для рендера. Широкий
|
|
||||||
# `_next/*` открыл бы анонимам ещё и `/_next/image` (оптимизация картинок,
|
|
||||||
# CPU-нагрузка по запросу), который на лэндинге не используется вообще:
|
|
||||||
# next/image в tradein-mvp/frontend/src/app/mera-public/ не импортируется.
|
|
||||||
handle /trade-in/_next/static/* {
|
|
||||||
reverse_proxy tradein-frontend:3000 {
|
|
||||||
header_up -X-Authenticated-User
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# #2631: favicon — единственный корневой статик, который браузер запрашивает
|
|
||||||
# сам; без явного handle падал в allowlist-404. app/favicon.ico отдаёт Next
|
|
||||||
# по корневому пути через basePath /trade-in.
|
|
||||||
handle /favicon.ico {
|
|
||||||
rewrite * /trade-in/favicon.ico
|
|
||||||
reverse_proxy tradein-frontend:3000 {
|
|
||||||
header_up -X-Authenticated-User
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Публичный API МЕРЫ — ЕДИНСТВЕННЫЙ путь этого домена, доходящий до
|
|
||||||
# бэкенда. Под /api/public/ по определению не лежит ничего закрытого:
|
|
||||||
# гарантию даёт структура пакета app/api/public/, а не аккуратность этого
|
|
||||||
# матчера (разбор — в app/api/public/mera.py). Матчер тем не менее узкий:
|
|
||||||
# /trade-in/api/v1/* по-прежнему падает в catch-all 404 ниже.
|
|
||||||
#
|
|
||||||
# ПОЧЕМУ ПУТЬ С ПРЕФИКСОМ /trade-in, А НЕ КОРОТКИЙ /api/public/*.
|
|
||||||
# Тот же URL обязан работать и на gendsgn.ru/trade-in/mera-public — ту же
|
|
||||||
# страницу оттуда открывают для QA (там она за basic_auth). На gendsgn.ru
|
|
||||||
# корневой /api/* уже занят бэкендом Site Finder, то есть короткий путь
|
|
||||||
# потребовал бы там ВТОРОГО handle, выигрывающего у существующего по
|
|
||||||
# специфичности — то есть работоспособность публичной формы зависела бы от
|
|
||||||
# порядка сортировки матчеров в чужом site-блоке. С префиксом /trade-in
|
|
||||||
# запрос ловит уже существующий `handle /trade-in/api/*` (:123), и здесь
|
|
||||||
# нужен ровно один новый handle. Цена — префикс /trade-in виден в devtools
|
|
||||||
# публичного домена; он там и так виден на всех чанках Next (basePath).
|
|
||||||
#
|
|
||||||
# strip_prefix — та же причина, что у B2B-хопа (:127): basePath Next'а не
|
|
||||||
# часть маршрута FastAPI.
|
|
||||||
#
|
|
||||||
# X-Internal-Auth-Secret здесь НЕ подставляется (в отличие от :130):
|
|
||||||
# публичные ручки его не проверяют, а инжектить внутренний секрет в хоп с
|
|
||||||
# анонимного домена — расширять доверие без нужды.
|
|
||||||
handle /trade-in/api/public/* {
|
|
||||||
uri strip_prefix /trade-in
|
|
||||||
reverse_proxy tradein-backend:8000 {
|
|
||||||
header_up -X-Authenticated-User
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Allowlist-by-default: любой другой путь (включая B2B — /v2, /admin,
|
|
||||||
# /scrapers/*, /trade-in/api/v1/*, /history, ...) — 404, НЕ проксируется.
|
|
||||||
handle {
|
|
||||||
respond 404
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Домены-спутники МЕРА → 301 на канонический meraocenka.ru.
|
|
||||||
# Решение 2026-07-31: канонический адрес ровно один, остальные две регистрации
|
|
||||||
# ловят (а) альтернативный транслит «оценка» — ocenka/otsenka, на слух
|
|
||||||
# неразличимы, (б) прежний рабочий вариант merahome. Отдельные site-блоки, а не
|
|
||||||
# matcher внутри основного: Caddy матчит по hostname и выпускает свой
|
|
||||||
# сертификат на каждый, поэтому DNS A-record нужен для КАЖДОГО из них — иначе
|
|
||||||
# ACME для этого хоста будет ретраиться (безвредно, см. комментарий выше, но
|
|
||||||
# лучше завести записи сразу).
|
|
||||||
# `{uri}` сохраняет путь и query — короткая ссылка с визитки не теряет ?id=.
|
|
||||||
merahome.ru {
|
|
||||||
redir https://meraocenka.ru{uri} permanent
|
|
||||||
}
|
|
||||||
|
|
||||||
meraotsenka.ru {
|
|
||||||
redir https://meraocenka.ru{uri} permanent
|
|
||||||
}
|
|
||||||
|
|
||||||
# Obsidian Self-hosted LiveSync (CouchDB backend).
|
|
||||||
# Auto-TLS Let's Encrypt. CORS уже включён на стороне CouchDB через bootstrap
|
|
||||||
# (см. scripts/setup-couchdb.sh). Basic-auth — на стороне CouchDB (admin user).
|
|
||||||
#
|
|
||||||
# DNS: A-record obsidian.gendsgn.ru → IP VPS.
|
|
||||||
# Клиенты Obsidian + Self-hosted LiveSync plugin указывают на этот URL.
|
|
||||||
obsidian.gendsgn.ru {
|
|
||||||
encode zstd gzip
|
|
||||||
|
|
||||||
reverse_proxy couchdb:5984 {
|
|
||||||
# Большие документы (vault attachments / images) — увеличиваем timeout
|
|
||||||
transport http {
|
|
||||||
response_header_timeout 120s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# GlitchTip — self-hosted error tracking (Sentry-compatible).
|
|
||||||
# DNS: A-record errors.gendsgn.ru → IP VPS.
|
|
||||||
errors.gendsgn.ru {
|
|
||||||
encode zstd gzip
|
|
||||||
|
|
||||||
reverse_proxy glitchtip-web:8080
|
|
||||||
|
|
||||||
log {
|
|
||||||
output file /var/log/caddy/errors.gendsgn.ru.log
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Forgejo — self-hosted git (migration 2026-05-16).
|
|
||||||
# DNS: A-record git.gendsgn.ru → IP VPS.
|
|
||||||
# Forgejo container из forgejo-migration/docker-compose.yml на shared
|
|
||||||
# gendesign_default network. HTTP port 3000 (default Forgejo).
|
|
||||||
# Был добавлен вручную при migration, потерян при первом auto-deploy после
|
|
||||||
# изменения Caddyfile (deploy.yml делает git reset --hard). См. fix issue.
|
|
||||||
git.gendsgn.ru {
|
|
||||||
encode zstd gzip
|
|
||||||
|
|
||||||
reverse_proxy forgejo:3000
|
|
||||||
|
|
||||||
log {
|
|
||||||
output file /var/log/caddy/git.gendsgn.ru.log
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Plain HTTP by IP — closed by same auth gate (prevent bypass via direct IP / SSH tunnel).
|
# Plain HTTP by IP — closed by same auth gate (prevent bypass via direct IP / SSH tunnel).
|
||||||
# Caddy issues no TLS here (no hostname). /health remains public.
|
# Caddy issues no TLS here (no hostname). /health remains public.
|
||||||
|
|
|
||||||
475
caddy/sites/apps.caddy
Normal file
475
caddy/sites/apps.caddy
Normal file
|
|
@ -0,0 +1,475 @@
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# caddy/sites/apps.caddy — домены Site Finder / МЕРА, переезжающие на Selectel
|
||||||
|
#
|
||||||
|
# Переезд GenDesign с Beget (46.173.16.127) на Selectel Poincare
|
||||||
|
# (188.246.224.93), окно 30.08.2026. Пять доменов из этого файла переезжают
|
||||||
|
# на новый VPS, три инфраструктурных (obsidian/errors/git.gendsgn.ru,
|
||||||
|
# см. caddy/sites/infra.caddy) остаются на Beget.
|
||||||
|
#
|
||||||
|
# Домены в этом файле (после переезда — на Selectel):
|
||||||
|
# - gendsgn.ru — Site Finder + Trade-In, основной прод-домен
|
||||||
|
# - www.gendsgn.ru — 301 на apex (канонический URL)
|
||||||
|
# - meraocenka.ru — публичный лендинг МЕРА B2C (канонический домен)
|
||||||
|
# - merahome.ru — 301 на meraocenka.ru (домен-спутник)
|
||||||
|
# - meraotsenka.ru — 301 на meraocenka.ru (домен-спутник, альт. транслит)
|
||||||
|
#
|
||||||
|
# ПОЧЕМУ ЭТОТ ФАЙЛ ВООБЩЕ СУЩЕСТВУЕТ (не один Caddyfile на оба хоста).
|
||||||
|
# Деплой делает git reset --hard на origin/main + `caddy reload` — на прод
|
||||||
|
# приезжает ВЕСЬ репозиторий, включая site-блоки для доменов, которых на этом
|
||||||
|
# хосте физически нет. Если бы этот файл жил как
|
||||||
|
# site-блоки прямо в корневом Caddyfile, Caddy на Selectel пытался бы
|
||||||
|
# выпускать сертификаты ACME для obsidian/errors/git.gendsgn.ru (DNS которых
|
||||||
|
# указывает на Beget) — challenge недостижим, попытки ретраятся с backoff и
|
||||||
|
# могут упереться в rate limit Let's Encrypt (5 failed validations/hostname/
|
||||||
|
# hour). Симметрично на Beget после переезда — попытки для gendsgn.ru и
|
||||||
|
# остальных доменов из этого файла. Разделение на apps.caddy / infra.caddy +
|
||||||
|
# переменная CADDY_SITES убирает эту проблему: каждый хост импортирует
|
||||||
|
# ТОЛЬКО свой набор доменов.
|
||||||
|
#
|
||||||
|
# КАК ПЕРЕКЛЮЧАЕТСЯ. Корневой Caddyfile содержит
|
||||||
|
# `import caddy/sites/{$CADDY_SITES:*}.caddy` — Caddy подставляет переменные
|
||||||
|
# окружения в текст Caddyfile ДО парсинга (включая пути import), поэтому
|
||||||
|
# значение CADDY_SITES определяет, какой файл(ы) реально импортируются:
|
||||||
|
# - CADDY_SITES не задан (default `*`) → import caddy/sites/*.caddy →
|
||||||
|
# ОБА файла, apps.caddy И infra.caddy — текущее поведение Beget ДО
|
||||||
|
# переезда, когда один VPS обслуживал все восемь доменов.
|
||||||
|
# - CADDY_SITES=apps (Selectel, после cutover) → только этот файл.
|
||||||
|
# - CADDY_SITES=infra (Beget, после cutover) → только infra.caddy.
|
||||||
|
# Переменная задаётся в environment секции caddy-сервиса в
|
||||||
|
# docker-compose.prod.yml (или runtime-переменной окружения на хосте) — по
|
||||||
|
# одному значению на хост, меняется руками один раз при переезде, не через
|
||||||
|
# код.
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
# Caddy config.
|
||||||
|
#
|
||||||
|
# - gendsgn.ru — main production site, auto-TLS via Let's Encrypt.
|
||||||
|
# - www.gendsgn.ru — 301 redirect to apex (canonical URL).
|
||||||
|
# - :80 — fallback for raw IP access. Closed by same auth gate (pilot phase).
|
||||||
|
#
|
||||||
|
# `handle /api/*` (NOT `handle_path`) — we keep the /api prefix because
|
||||||
|
# the FastAPI router is mounted at /api/v1/*.
|
||||||
|
#
|
||||||
|
# Auth gate: basic_auth for pilot phase (2026-05-23).
|
||||||
|
# Users managed via caddy/users.caddy.snippet (git history = audit trail).
|
||||||
|
# Public exclusions: /health (liveness probe), /preview/* (static mockups).
|
||||||
|
#
|
||||||
|
# #2558: с 2026-07 basic_auth гейтит ТОЛЬКО Site Finder (`/`, `/api/*`,
|
||||||
|
# `/analytics` и т.д.). `/trade-in/*` (+ `/sale-share` redirect) вынесены ВЫШЕ
|
||||||
|
# import'а — у trade-in своя авторизация (форма входа + opaque session-cookie,
|
||||||
|
# см. #2552) поверх RBAC (`tradein-mvp/backend/app/core/rbac.py`). Site Finder
|
||||||
|
# всё ещё легаси-пилотный basic_auth (roles.yaml dual-mode остаётся живым для
|
||||||
|
# него — НЕ трогать caddy/users.caddy.snippet).
|
||||||
|
#
|
||||||
|
# IMPORTANT: route { } block is required to preserve directive order.
|
||||||
|
# Without route { }, Caddy executes directives in hard-coded default order
|
||||||
|
# (basic_auth runs before handle), making /health and /preview/* exclusions
|
||||||
|
# ineffective. With route { }, handlers are matched top-to-bottom as written.
|
||||||
|
|
||||||
|
# #2213: global `log_credentials` УБРАН. Он заставлял Caddy писать raw
|
||||||
|
# Authorization header (Base64 `user:password`, тривиально обратимый) в ОБА лог-файла
|
||||||
|
# (gendsgn.ru.log + auth_audit.log). Даже при root-only доступе и коротком retention
|
||||||
|
# это plaintext-пароли пилотов на диске — недопустимый риск против скромной выгоды.
|
||||||
|
# Trade-off: glitchtip-auth-forwarder больше не извлекает `attempted_username` из 401
|
||||||
|
# (тег деградирует в "(none)" — forwarder это уже обрабатывает gracefully, не падает).
|
||||||
|
# Событие basic_auth 401 (remote_ip / uri / method) по-прежнему уходит в GlitchTip.
|
||||||
|
|
||||||
|
gendsgn.ru {
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
log {
|
||||||
|
output file /var/log/caddy/gendsgn.ru.log {
|
||||||
|
roll_size 50MiB
|
||||||
|
roll_keep 5
|
||||||
|
roll_keep_for 720h
|
||||||
|
}
|
||||||
|
format json
|
||||||
|
}
|
||||||
|
|
||||||
|
# Отдельный лог только для auth-событий.
|
||||||
|
# Forwarder (ops/glitchtip-auth-forwarder) читает именно этот файл.
|
||||||
|
# Retention 7 дней (меньше чем main log) — содержит plain Base64 credentials.
|
||||||
|
log auth_audit {
|
||||||
|
output file /var/log/caddy/auth_audit.log {
|
||||||
|
roll_size 10MiB
|
||||||
|
roll_keep 3
|
||||||
|
roll_keep_for 168h
|
||||||
|
}
|
||||||
|
format json
|
||||||
|
}
|
||||||
|
|
||||||
|
route {
|
||||||
|
# /health и /preview/* — public, без auth, short-circuit.
|
||||||
|
handle /health {
|
||||||
|
reverse_proxy backend:8000
|
||||||
|
}
|
||||||
|
|
||||||
|
# Static HTML mockups для review (audit alternatives).
|
||||||
|
# Public access — без auth (по запросу 2026-05-17).
|
||||||
|
handle_path /preview/* {
|
||||||
|
root * /srv/preview
|
||||||
|
file_server browse
|
||||||
|
}
|
||||||
|
|
||||||
|
# Trade-In UI preview — public CI surface (#801). Рендерит mock-фикстуру
|
||||||
|
# «денежного экрана» без бэкенда → axe/lighthouse гоняются без креды.
|
||||||
|
# Реальных клиентских данных нет (статичная фикстура) → безопасно публично.
|
||||||
|
# ДО auth-import: route матчит сверху вниз, handle short-circuit'ит.
|
||||||
|
# Без strip — Next.js basePath=/trade-in ждёт префикс в URL (как @tradein).
|
||||||
|
# ui-preview + его статика (_next/static — CSS/JS бандлы, без секретов).
|
||||||
|
# Оба ДО auth-import, иначе ассеты страницы уходят в @tradein (под auth) → 401 → без CSS.
|
||||||
|
@uipreview path /trade-in/ui-preview/* /trade-in/_next/static/*
|
||||||
|
handle @uipreview {
|
||||||
|
reverse_proxy tradein-frontend:3000 {
|
||||||
|
# #2558 review: тот же периметр-scrub, что и у /trade-in/api/* и
|
||||||
|
# @tradein ниже — этот блок тоже теперь ДО basic_auth, клиент
|
||||||
|
# мог бы прислать свой X-Authenticated-User. Сейчас инертно
|
||||||
|
# (страница статична, у tradein-frontend нет секрета для
|
||||||
|
# X-Internal-Auth-Secret), но убираем ради единообразия периметра,
|
||||||
|
# а не полагаясь на то, что downstream ничего не делает с заголовком.
|
||||||
|
header_up -X-Authenticated-User
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# #2558: Trade-In MVP subproject (tradein-mvp/) — gendesign-tradein docker
|
||||||
|
# stack, подключен через gendesign_shared network. Секция ЦЕЛИКОМ ДО
|
||||||
|
# `import caddy/users.caddy.snippet` ниже — /trade-in имеет собственную
|
||||||
|
# авторизацию (форма входа + opaque session-cookie, #2552; RBAC-проверка
|
||||||
|
# роли внутри tradein-backend, `app/core/rbac.py`), Site Finder basic_auth
|
||||||
|
# ей больше не нужен и не должен применяться (short-circuit сверху вниз,
|
||||||
|
# как /health и /preview/* выше).
|
||||||
|
#
|
||||||
|
# X-Authenticated-User — ЯВНОЕ УДАЛЕНИЕ (`header_up -X-Authenticated-User`),
|
||||||
|
# НЕ `header_up X-Authenticated-User {http.auth.user.id}`. Причина: этот
|
||||||
|
# блок больше не идёт ПОСЛЕ basic_auth, поэтому `{http.auth.user.id}`
|
||||||
|
# никогда не резолвится авторизованным юзером на этом пути.
|
||||||
|
# Проверено эмпирически (echo-стенд на образе caddy:2, `caddy adapt`):
|
||||||
|
# старая Set-форма (`header_up X-Authenticated-User {http.auth.user.id}`)
|
||||||
|
# НЕ пропустила бы клиентский заголовок насквозь и НЕ оставила бы поле
|
||||||
|
# пустым — Caddy подставляет НЕРАЗРЕШЁННЫЙ плейсхолдер как ЛИТЕРАЛЬНУЮ
|
||||||
|
# строку (`ReplaceKnown`), т.е. upstream получил бы буквально
|
||||||
|
# `X-Authenticated-User: {http.auth.user.id}`. Для backend (auth_mode=
|
||||||
|
# "dual", `app/core/config.py`) это НЕ подмена личности — legacy path
|
||||||
|
# (`rbac.py:186`) сделал бы `get_role("{http.auth.user.id}")`, юзер не
|
||||||
|
# найден в roles.yaml → 403 для всех. Т.е. старая форма была бы не
|
||||||
|
# security-дырой, а fail-closed-but-сломанной (все trade-in запросы без
|
||||||
|
# session-cookie получали бы 403 вместо ожидаемого 401/редиректа на логин).
|
||||||
|
# `-Field` остаётся правильным выбором не потому что Set был бы дырой, а
|
||||||
|
# потому что это ЕДИНСТВЕННАЯ форма с явно задокументированной семантикой
|
||||||
|
# "удалить заголовок" (Caddyfile reverse_proxy directive: `-<field>` =
|
||||||
|
# delete) — корректное поведение не должно зависеть от того, как именно
|
||||||
|
# Caddy трактует нерезолвленный/пустой плейсхолдер в Set-операции.
|
||||||
|
# X-Internal-Auth-Secret НЕ трогаем — #2213-секрет всегда перезаписывается
|
||||||
|
# из env (Set-операция с непустым значением, никак не связана с auth-гейтом
|
||||||
|
# basic_auth), это единственное, что теперь отсекает подделку заголовков
|
||||||
|
# изнутри gendesign_shared network для legacy dual-mode пути.
|
||||||
|
handle /trade-in/api/* {
|
||||||
|
# `handle_path /trade-in/api/*` стрипал бы целиком /trade-in/api;
|
||||||
|
# FastAPI router замаунтен на /api/v1/trade-in/* — нужен strip только
|
||||||
|
# префикса basePath /trade-in (Next.js basePath leak).
|
||||||
|
uri strip_prefix /trade-in
|
||||||
|
reverse_proxy tradein-backend:8000 {
|
||||||
|
header_up -X-Authenticated-User
|
||||||
|
header_up X-Internal-Auth-Secret {env.TRADEIN_INTERNAL_AUTH_SECRET}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# gendsgn.ru/sale-share — короткий адрес standalone-продукта «Поиск домов».
|
||||||
|
# Next basePath=/trade-in → редиректим на канонический /trade-in/sale-share
|
||||||
|
# (тот же tradein-frontend контейнер; query-string сохраняется). True vanity-URL
|
||||||
|
# в адресной строке требует отдельного Next-app с basePath=/sale-share.
|
||||||
|
# #2558: перенесён ВЫШЕ auth-import вместе с trade-in — редирект ведёт на
|
||||||
|
# /trade-in/sale-share, для которого теперь нет Caddy basic_auth (как и
|
||||||
|
# для остального /trade-in). Это НЕ делает страницу публичной: она всё
|
||||||
|
# ещё за собственной авторизацией trade-in — `RouteGuard` во фронте
|
||||||
|
# (`app/layout.tsx`) и сессия для `/api/v1/buildings/sale-share*` на
|
||||||
|
# бэке; без валидной сессии юзер получит редирект на /login, а не
|
||||||
|
# контент. Смысл переноса — не открыть страницу всем, а убрать
|
||||||
|
# несогласованность: короткий URL не должен быть строже (Caddy
|
||||||
|
# basic_auth) целевого адреса, к которому и так уже нет
|
||||||
|
# basic_auth-барьера (только собственный login trade-in).
|
||||||
|
#
|
||||||
|
# ОБНОВЛЕНО 2026-07-31: доступ к разделу сузился с «pilot + admin» до
|
||||||
|
# ТОЛЬКО admin — «Поиск домов» признан тестовым продуктом, клиентам не
|
||||||
|
# показывается (deny в auth/roles.yaml для pilot и analyst + в
|
||||||
|
# DB_ROLE_PATHS для employee/manager). Сам редирект не трогаем: он ведёт
|
||||||
|
# на страницу, а гейт стоит на роли — для всех, кроме admin, короткий
|
||||||
|
# адрес приведёт на NoAccessScreen.
|
||||||
|
@saleshare path /sale-share /sale-share/
|
||||||
|
handle @saleshare {
|
||||||
|
redir /trade-in/sale-share permanent
|
||||||
|
}
|
||||||
|
|
||||||
|
# Matcher `path /trade-in /trade-in/*` ловит И /trade-in (без слеша),
|
||||||
|
# И /trade-in/ + /trade-in/anything. Без обоих случаев `handle /trade-in/*`
|
||||||
|
# пропускал /trade-in без слеша → попадал в общий frontend → пустой ответ.
|
||||||
|
@tradein path /trade-in /trade-in/*
|
||||||
|
handle @tradein {
|
||||||
|
# Next.js basePath=/trade-in — фронт сам ждёт префикса в URL
|
||||||
|
reverse_proxy tradein-frontend:3000 {
|
||||||
|
# См. комментарий над /trade-in/api/* выше — та же логика (явное
|
||||||
|
# удаление вместо Set с пустым {http.auth.user.id}).
|
||||||
|
header_up -X-Authenticated-User
|
||||||
|
header_up X-Internal-Auth-Secret {env.TRADEIN_INTERNAL_AUTH_SECRET}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Auth gate — с #2558 применяется ТОЛЬКО к Site Finder (handle /api/* и
|
||||||
|
# handle {} ниже). Trade-In уже отработал и short-circuit'нул выше.
|
||||||
|
import ../users.caddy.snippet
|
||||||
|
|
||||||
|
handle /api/* {
|
||||||
|
reverse_proxy backend:8000 {
|
||||||
|
header_up X-Authenticated-User {http.auth.user.id}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handle {
|
||||||
|
reverse_proxy frontend:3000 {
|
||||||
|
header_up X-Authenticated-User {http.auth.user.id}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
www.gendsgn.ru {
|
||||||
|
redir https://gendsgn.ru{uri} permanent
|
||||||
|
}
|
||||||
|
|
||||||
|
# МЕРА B2C — публичный периметр (ЭТАП 1 плана B2C-запуска, БЕЗ функционала).
|
||||||
|
#
|
||||||
|
# Архитектурное решение: отдельный домен, а НЕ дырка в блоке gendsgn.ru
|
||||||
|
# выше. На gendsgn.ru модель "запрещено всё, кроме дырок ВЫШЕ auth-import" —
|
||||||
|
# порядко-зависимая и общая для B2B (trade-in v2, admin, scrapers, /api/*).
|
||||||
|
# Здесь, наоборот, allowlist-by-default: basic_auth НЕТ ВООБЩЕ (не импортируем
|
||||||
|
# caddy/users.caddy.snippet), потому что на этом site-блоке B2B-маршрутов
|
||||||
|
# физически не объявлено — их нечего "открывать". Явно перечислены РОВНО два
|
||||||
|
# handle (корень "/" + статика Next _next/*), всё остальное — финальный
|
||||||
|
# catch-all `handle { respond 404 }`. Регресс-тест на эту модель:
|
||||||
|
# scripts/smoke-mera-perimeter.sh (проверяет, что B2B-путь здесь = 404, а не
|
||||||
|
# 200/401 — т.е. не был случайно проброшен).
|
||||||
|
#
|
||||||
|
# Next.js basePath=/trade-in запечён в prod-образ tradein-frontend (тот же
|
||||||
|
# контейнер, что обслуживает и gendsgn.ru/trade-in/*, см. build-args в
|
||||||
|
# .forgejo/workflows/deploy-tradein.yml) — поэтому корень домена rewrite'ится
|
||||||
|
# на internal-путь /trade-in/mera-public (страница-заглушка,
|
||||||
|
# tradein-mvp/frontend/src/app/mera-public/). Пользователь префикс /trade-in
|
||||||
|
# никогда не видит — rewrite меняет путь ТОЛЬКО для Caddy→backend запроса,
|
||||||
|
# это не HTTP-редирект браузера.
|
||||||
|
#
|
||||||
|
# DNS: A-record meraocenka.ru → IP VPS — ТРЕБУЕТСЯ ДО того, как сюда придёт
|
||||||
|
# реальный трафик. Если записи ещё нет на момент деплоя этого блока: `caddy
|
||||||
|
# reload`/`up -d --force-recreate caddy` в deploy.yml НЕ падает (конфиг
|
||||||
|
# синтаксически валиден, ошибка сертификата асинхронна и per-hostname) — Caddy
|
||||||
|
# просто залогирует неудачную попытку ACME-выпуска для meraocenka.ru (DNS не
|
||||||
|
# резолвится на этот сервер → HTTP-01/TLS-ALPN challenge недостижим) и продолжит
|
||||||
|
# ретраить с backoff, ПОКА запись не появится. Остальные site-блоки в этом же
|
||||||
|
# Caddyfile (gendsgn.ru, obsidian.gendsgn.ru и т.д.) не затрагиваются —
|
||||||
|
# автоматический HTTPS в Caddy изолирован per-hostname. Повторные неудачные попытки ДО
|
||||||
|
# появления DNS могут исчерпать rate-limit Let's Encrypt (5 failed
|
||||||
|
# validations/hostname/hour) — не критично, просто подождать; `docker volume
|
||||||
|
# rm gendesign_caddy_data` для этого НЕ нужен (и вообще требует user-approval).
|
||||||
|
meraocenka.ru {
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
log {
|
||||||
|
output file /var/log/caddy/meraocenka.ru.log
|
||||||
|
}
|
||||||
|
|
||||||
|
# Корень домена → лэндинг МЕРЫ (#2615 заменил заглушку этого этапа на
|
||||||
|
# полноценную страницу). rewrite добавляет basePath-префикс только для
|
||||||
|
# Caddy→backend хопа, пользователь /trade-in никогда не видит.
|
||||||
|
handle / {
|
||||||
|
rewrite * /trade-in/mera-public
|
||||||
|
reverse_proxy tradein-frontend:3000 {
|
||||||
|
# Тот же периметр-скраб, что у @uipreview (:87) и @tradein ниже.
|
||||||
|
# Этот блок вообще не под basic_auth, поэтому анонимный клиент
|
||||||
|
# тем более может прислать свой X-Authenticated-User. Сейчас
|
||||||
|
# инертно (лэндинг статичен, backend-вызовов нет), но снимаем
|
||||||
|
# ради единообразия периметра, а не полагаясь на то, что
|
||||||
|
# downstream ничего не делает с заголовком — иначе на этапе 5,
|
||||||
|
# когда откроется публичный /estimate, это станет дырой.
|
||||||
|
header_up -X-Authenticated-User
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Короткие адреса страниц публичного сайта. Именно они напечатаны ВНУТРИ
|
||||||
|
# юридических документов (оферта ссылается на meraocenka.ru/refund,
|
||||||
|
# политика возврата — на meraocenka.ru/oferta) и уходят в заявку эквайеру,
|
||||||
|
# поэтому обязаны резолвиться сами по себе.
|
||||||
|
#
|
||||||
|
# ЭТО ЕДИНСТВЕННЫЙ ВИД АДРЕСА, КОТОРЫЙ ВИДИТ ЧЕЛОВЕК (решение владельца,
|
||||||
|
# 15.08.2026). Раньше навигация внутри сайта ходила по длинным
|
||||||
|
# /trade-in/mera-public/... — так короткие адреса и длинные существовали
|
||||||
|
# параллельно. Теперь длинные отдают 301 на короткие (см. handle ниже), а
|
||||||
|
# ссылки на страницах эмитятся сразу короткими (см. `PublicLink` во
|
||||||
|
# фронте — обычный <a>, потому что next/link подставляет basePath).
|
||||||
|
#
|
||||||
|
# `rewrite`, а не `redir`: адрес в строке браузера должен остаться коротким
|
||||||
|
# — модератор эквайера открывает ссылку из заявки и видит ровно тот URL,
|
||||||
|
# который в ней указан. Каноничность для поисковиков задана отдельно, через
|
||||||
|
# `alternates.canonical` на каждой странице.
|
||||||
|
#
|
||||||
|
# Пути перечислены поимённо, а не шаблоном: allowlist-by-default этого
|
||||||
|
# site-блока — часть периметра (#2545), и превращать его в «любой корневой
|
||||||
|
# путь проксируется» нельзя. Новая публичная страница = новая строка здесь
|
||||||
|
# (и проверка в scripts/smoke-mera-perimeter.sh).
|
||||||
|
#
|
||||||
|
# NB: корень «/» СЮДА НЕ ВХОДИТ — он выше, отдельным handle. Причина
|
||||||
|
# техническая: здесь цель собирается как `/trade-in/mera-public{path}`, а
|
||||||
|
# для «/» это дало бы `/trade-in/mera-public/` со слэшем на конце. Next при
|
||||||
|
# `trailingSlash: false` ответил бы на такой путь 308-редиректом на вариант
|
||||||
|
# без слэша — то есть на ДЛИННЫЙ адрес, который handle ниже отправит 301 на
|
||||||
|
# «/», и запрос закольцуется.
|
||||||
|
# `/v3` — ВРЕМЕННОЕ превью второго варианта дизайна, а не публичная
|
||||||
|
# страница: владелец сравнивает его с текущим лэндингом. Оно `noindex` и
|
||||||
|
# ни с одной страницы на него нет ссылки. Убрать эту строку в тот момент,
|
||||||
|
# когда вариант выберут и он станет корнем.
|
||||||
|
@meraPages path /estimate /oferta /refund /privacy /v3
|
||||||
|
handle @meraPages {
|
||||||
|
rewrite * /trade-in/mera-public{path}
|
||||||
|
reverse_proxy tradein-frontend:3000 {
|
||||||
|
header_up -X-Authenticated-User
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Тот же адрес со слэшем на конце → 301 на канонический вид без слэша.
|
||||||
|
# Слэш дописывают мессенджеры, автолинкификаторы и сами люди, а матчер
|
||||||
|
# `path` требует точного совпадения — без этой ветки `/oferta/` отдавал бы
|
||||||
|
# голый 404 (так было и до этого PR, с момента #2615). Заодно это
|
||||||
|
# замыкает цепочку для длинных адресов со слэшем: они приходят на короткий
|
||||||
|
# со слэшем и здесь нормализуются.
|
||||||
|
@meraShortSlash path_regexp shortslash ^/(estimate|oferta|refund|privacy|v3)/$
|
||||||
|
handle @meraShortSlash {
|
||||||
|
redir * /{re.shortslash.1} permanent
|
||||||
|
}
|
||||||
|
|
||||||
|
# Длинные адреса поддерева → 301 на короткие. Один канонический адрес у
|
||||||
|
# страницы, а не два работающих.
|
||||||
|
#
|
||||||
|
# Зачем вообще оставлять длинные: они уже разошлись — ими ссылались подвал
|
||||||
|
# и шапка до 15.08.2026, они могли попасть в закладки и в переписку. 301
|
||||||
|
# (а не 404) сохраняет эти ссылки живыми и заодно передаёт поисковикам, что
|
||||||
|
# канонический адрес один.
|
||||||
|
#
|
||||||
|
# ЗДЕСЬ ЖЕ ЧИНИТСЯ БАГ: прежний матчер был `/trade-in/mera-public/*` — со
|
||||||
|
# слэшем и звёздочкой, поэтому ГОЛЫЙ `/trade-in/mera-public` (без хвоста)
|
||||||
|
# под него не подпадал и падал в catch-all 404. Ровно на этот адрес вела
|
||||||
|
# ссылка «Главная» в подвале v3, то есть она была мёртвой (замер на проде
|
||||||
|
# 15.08.2026). Первый матчер ниже ловит обе формы — со слэшем и без.
|
||||||
|
#
|
||||||
|
# `redir * <куда>`, а НЕ `redir <куда>`. Первый аргумент директивы, если он
|
||||||
|
# начинается со слэша, Caddy разбирает как inline path-matcher — то есть
|
||||||
|
# `redir / permanent` означает «для пути / редиректить на permanent», а не
|
||||||
|
# «редиректить на /». Проверено на живом Caddy: без `*` длинные адреса
|
||||||
|
# отдавали пустой 200 (матчер не совпадал, директива не срабатывала, тело
|
||||||
|
# пустое) — хуже, чем 404, потому что выглядит как рабочая пустая страница.
|
||||||
|
@meraLongRoot path /trade-in/mera-public /trade-in/mera-public/
|
||||||
|
handle @meraLongRoot {
|
||||||
|
redir * / permanent
|
||||||
|
}
|
||||||
|
|
||||||
|
# Длинные адреса страниц → короткие. Пути перечислены ПОИМЁННО, обе формы
|
||||||
|
# (со слэшем на конце и без) — не шаблоном и не регекспом.
|
||||||
|
#
|
||||||
|
# ПОЧЕМУ НЕ РЕГЕКСП С ЗАХВАТОМ ХВОСТА. Очевидный вариант
|
||||||
|
# `path_regexp ^/trade-in/mera-public/(.+)$` + `redir /{re.…1}` — открытый
|
||||||
|
# редирект. Захват берётся из РАСКОДИРОВАННОГО пути, поэтому
|
||||||
|
# `/trade-in/mera-public/%5Cevil.example/pay` даёт цель `/\evil.example/pay`,
|
||||||
|
# а браузеры трактуют `/\` как `//` — Location уводит на ЧУЖОЙ хост. Это
|
||||||
|
# готовая фишинговая заготовка с домена, который напечатан внутри оферты и
|
||||||
|
# уходит модератору эквайера. Проверено на живом Caddy, воспроизводится.
|
||||||
|
# С поимённым списком такой путь просто не матчится и падает в 404 ниже.
|
||||||
|
#
|
||||||
|
# ПОЧЕМУ `uri strip_prefix` + `{uri}`, А НЕ `redir /oferta` в каждой ветке.
|
||||||
|
# `{uri}` переносит query-строку: уже размещённые ссылки с UTM-метками
|
||||||
|
# после редиректа не теряют атрибуцию. Обёртка `route` обязательна —
|
||||||
|
# порядок директив внутри `handle` определяет Caddy, и без неё `redir`
|
||||||
|
# выполняется РАНЬШЕ `uri`, отдавая Location, равный исходному адресу
|
||||||
|
# (бесконечный цикл; поймано на локальном стенде).
|
||||||
|
@meraLongPages path /trade-in/mera-public/estimate /trade-in/mera-public/estimate/ /trade-in/mera-public/oferta /trade-in/mera-public/oferta/ /trade-in/mera-public/refund /trade-in/mera-public/refund/ /trade-in/mera-public/privacy /trade-in/mera-public/privacy/ /trade-in/mera-public/v3 /trade-in/mera-public/v3/
|
||||||
|
handle @meraLongPages {
|
||||||
|
route {
|
||||||
|
uri strip_prefix /trade-in/mera-public
|
||||||
|
redir * {uri} permanent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Next.js уже эмитит ссылки на статику с /trade-in-префиксом (тот же
|
||||||
|
# basePath) — passthrough без rewrite. Нужны для рендера страницы (JS/CSS
|
||||||
|
# чанки), сами по себе не содержат ни B2B-данных, ни секретов.
|
||||||
|
#
|
||||||
|
# Именно `static/*`, а не весь `_next/*` — тот же матчер, что у @uipreview
|
||||||
|
# (:78), который в проде доказал, что этого хватает для рендера. Широкий
|
||||||
|
# `_next/*` открыл бы анонимам ещё и `/_next/image` (оптимизация картинок,
|
||||||
|
# CPU-нагрузка по запросу), который на лэндинге не используется вообще:
|
||||||
|
# next/image в tradein-mvp/frontend/src/app/mera-public/ не импортируется.
|
||||||
|
handle /trade-in/_next/static/* {
|
||||||
|
reverse_proxy tradein-frontend:3000 {
|
||||||
|
header_up -X-Authenticated-User
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# #2631: favicon — единственный корневой статик, который браузер запрашивает
|
||||||
|
# сам; без явного handle падал в allowlist-404. app/favicon.ico отдаёт Next
|
||||||
|
# по корневому пути через basePath /trade-in.
|
||||||
|
handle /favicon.ico {
|
||||||
|
rewrite * /trade-in/favicon.ico
|
||||||
|
reverse_proxy tradein-frontend:3000 {
|
||||||
|
header_up -X-Authenticated-User
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Публичный API МЕРЫ — ЕДИНСТВЕННЫЙ путь этого домена, доходящий до
|
||||||
|
# бэкенда. Под /api/public/ по определению не лежит ничего закрытого:
|
||||||
|
# гарантию даёт структура пакета app/api/public/, а не аккуратность этого
|
||||||
|
# матчера (разбор — в app/api/public/mera.py). Матчер тем не менее узкий:
|
||||||
|
# /trade-in/api/v1/* по-прежнему падает в catch-all 404 ниже.
|
||||||
|
#
|
||||||
|
# ПОЧЕМУ ПУТЬ С ПРЕФИКСОМ /trade-in, А НЕ КОРОТКИЙ /api/public/*.
|
||||||
|
# Тот же URL обязан работать и на gendsgn.ru/trade-in/mera-public — ту же
|
||||||
|
# страницу оттуда открывают для QA (там она за basic_auth). На gendsgn.ru
|
||||||
|
# корневой /api/* уже занят бэкендом Site Finder, то есть короткий путь
|
||||||
|
# потребовал бы там ВТОРОГО handle, выигрывающего у существующего по
|
||||||
|
# специфичности — то есть работоспособность публичной формы зависела бы от
|
||||||
|
# порядка сортировки матчеров в чужом site-блоке. С префиксом /trade-in
|
||||||
|
# запрос ловит уже существующий `handle /trade-in/api/*` (:123), и здесь
|
||||||
|
# нужен ровно один новый handle. Цена — префикс /trade-in виден в devtools
|
||||||
|
# публичного домена; он там и так виден на всех чанках Next (basePath).
|
||||||
|
#
|
||||||
|
# strip_prefix — та же причина, что у B2B-хопа (:127): basePath Next'а не
|
||||||
|
# часть маршрута FastAPI.
|
||||||
|
#
|
||||||
|
# X-Internal-Auth-Secret здесь НЕ подставляется (в отличие от :130):
|
||||||
|
# публичные ручки его не проверяют, а инжектить внутренний секрет в хоп с
|
||||||
|
# анонимного домена — расширять доверие без нужды.
|
||||||
|
handle /trade-in/api/public/* {
|
||||||
|
uri strip_prefix /trade-in
|
||||||
|
reverse_proxy tradein-backend:8000 {
|
||||||
|
header_up -X-Authenticated-User
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Allowlist-by-default: любой другой путь (включая B2B — /v2, /admin,
|
||||||
|
# /scrapers/*, /trade-in/api/v1/*, /history, ...) — 404, НЕ проксируется.
|
||||||
|
handle {
|
||||||
|
respond 404
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Домены-спутники МЕРА → 301 на канонический meraocenka.ru.
|
||||||
|
# Решение 2026-07-31: канонический адрес ровно один, остальные две регистрации
|
||||||
|
# ловят (а) альтернативный транслит «оценка» — ocenka/otsenka, на слух
|
||||||
|
# неразличимы, (б) прежний рабочий вариант merahome. Отдельные site-блоки, а не
|
||||||
|
# matcher внутри основного: Caddy матчит по hostname и выпускает свой
|
||||||
|
# сертификат на каждый, поэтому DNS A-record нужен для КАЖДОГО из них — иначе
|
||||||
|
# ACME для этого хоста будет ретраиться (безвредно, см. комментарий выше, но
|
||||||
|
# лучше завести записи сразу).
|
||||||
|
# `{uri}` сохраняет путь и query — короткая ссылка с визитки не теряет ?id=.
|
||||||
|
merahome.ru {
|
||||||
|
redir https://meraocenka.ru{uri} permanent
|
||||||
|
}
|
||||||
|
|
||||||
|
meraotsenka.ru {
|
||||||
|
redir https://meraocenka.ru{uri} permanent
|
||||||
|
}
|
||||||
64
caddy/sites/infra.caddy
Normal file
64
caddy/sites/infra.caddy
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
# caddy/sites/infra.caddy — инфраструктурные домены, остающиеся на Beget
|
||||||
|
#
|
||||||
|
# Переезд GenDesign с Beget (46.173.16.127) на Selectel Poincare
|
||||||
|
# (188.246.224.93), окно 30.08.2026. Три домена этого файла ОСТАЮТСЯ на
|
||||||
|
# Beget — переезжают только Site Finder/МЕРА-домены (см.
|
||||||
|
# caddy/sites/apps.caddy). Общий механизм переключения (переменная
|
||||||
|
# CADDY_SITES, `import caddy/sites/{$CADDY_SITES:*}.caddy` в корневом
|
||||||
|
# Caddyfile, мотив с ACME rate-limit) описан в шапке apps.caddy — не
|
||||||
|
# дублируем здесь, читай там.
|
||||||
|
#
|
||||||
|
# Домены в этом файле (после переезда — на Beget, CADDY_SITES=infra):
|
||||||
|
# - obsidian.gendsgn.ru — Obsidian Self-hosted LiveSync (CouchDB backend)
|
||||||
|
# - errors.gendsgn.ru — GlitchTip, self-hosted error tracking
|
||||||
|
# - git.gendsgn.ru — Forgejo, self-hosted git + Actions CI
|
||||||
|
#
|
||||||
|
# На Selectel (CADDY_SITES=apps) этот файл не импортируется вообще — Caddy
|
||||||
|
# там даже не видит эти хосты и не пытается выпускать для них сертификаты.
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
# Obsidian Self-hosted LiveSync (CouchDB backend).
|
||||||
|
# Auto-TLS Let's Encrypt. CORS уже включён на стороне CouchDB через bootstrap
|
||||||
|
# (см. scripts/setup-couchdb.sh). Basic-auth — на стороне CouchDB (admin user).
|
||||||
|
#
|
||||||
|
# DNS: A-record obsidian.gendsgn.ru → IP VPS.
|
||||||
|
# Клиенты Obsidian + Self-hosted LiveSync plugin указывают на этот URL.
|
||||||
|
obsidian.gendsgn.ru {
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
reverse_proxy couchdb:5984 {
|
||||||
|
# Большие документы (vault attachments / images) — увеличиваем timeout
|
||||||
|
transport http {
|
||||||
|
response_header_timeout 120s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# GlitchTip — self-hosted error tracking (Sentry-compatible).
|
||||||
|
# DNS: A-record errors.gendsgn.ru → IP VPS.
|
||||||
|
errors.gendsgn.ru {
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
reverse_proxy glitchtip-web:8080
|
||||||
|
|
||||||
|
log {
|
||||||
|
output file /var/log/caddy/errors.gendsgn.ru.log
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Forgejo — self-hosted git (migration 2026-05-16).
|
||||||
|
# DNS: A-record git.gendsgn.ru → IP VPS.
|
||||||
|
# Forgejo container из forgejo-migration/docker-compose.yml на shared
|
||||||
|
# gendesign_default network. HTTP port 3000 (default Forgejo).
|
||||||
|
# Был добавлен вручную при migration, потерян при первом auto-deploy после
|
||||||
|
# изменения Caddyfile (deploy.yml делает git reset --hard). См. fix issue.
|
||||||
|
git.gendsgn.ru {
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
reverse_proxy forgejo:3000
|
||||||
|
|
||||||
|
log {
|
||||||
|
output file /var/log/caddy/git.gendsgn.ru.log
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -441,6 +441,15 @@ services:
|
||||||
# держится в git через caddy/local/.gitignore — иначе docker создал бы
|
# держится в git через caddy/local/.gitignore — иначе docker создал бы
|
||||||
# отсутствующий bind-source сам, root-owned пустышкой.
|
# отсутствующий bind-source сам, root-owned пустышкой.
|
||||||
- ./caddy/local:/etc/caddy/caddy/local:ro
|
- ./caddy/local:/etc/caddy/caddy/local:ro
|
||||||
|
# Site-блоки, разложенные по хостам (#3059). Каддифайл импортирует их как
|
||||||
|
# `import caddy/sites/{$CADDY_SITES:*}.caddy` — без переменной берутся оба
|
||||||
|
# файла (текущее поведение Beget, все восемь доменов), CADDY_SITES=apps на
|
||||||
|
# Selectel даёт пять уезжающих, =infra на Beget после переезда — три
|
||||||
|
# остающихся (git / errors / obsidian).
|
||||||
|
# ⚠️ ПУСТОЕ значение переменной — не то же самое, что «не задана»: Caddy
|
||||||
|
# подставит пустую строку, получится путь caddy/sites/.caddy и конфиг не
|
||||||
|
# соберётся. Либо не задавать вовсе, либо задавать apps/infra.
|
||||||
|
- ./caddy/sites:/etc/caddy/caddy/sites:ro
|
||||||
- ./preview:/srv/preview:ro
|
- ./preview:/srv/preview:ro
|
||||||
- caddy_data:/data
|
- caddy_data:/data
|
||||||
- caddy_config:/config
|
- caddy_config:/config
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue