refactor(caddy): разделить site-блоки по хостам под переезд #3062

Merged
lekss361 merged 3 commits from refactor/caddyfile-split-by-host into main 2026-08-23 21:52:48 +00:00
Owner

Зачем

Пункт 4 из чеклиста переезда. Корневой Caddyfile содержал все восемь доменов в одном файле, а деплой синхронизирует рабочее дерево с origin/main и перечитывает конфиг целиком. Значит на Selectel приехал бы файл со всеми восемью — и Caddy начал бы выпускать сертификаты для obsidian / errors / git, чей DNS указывает на Beget. ACME падал бы на HTTP-01, с риском упереться в rate limit Let's Encrypt.

Вторая, более тяжёлая сторона той же проблемы (найдена при разведке порядка остановки/запуска, см. #3061): сейчас этот Caddy — единственный публичный вход и для тех трёх доменов тоже. Он уезжает, а Forgejo, GlitchTip и CouchDB остаются на Beget. Без разделения они отдавали бы 502, оставаясь при этом живыми — то есть отвалился бы и git, и приём ошибок, и синк волта, причём в момент переезда, когда они нужнее всего. Теперь на Beget остаётся Caddy с CADDY_SITES=infra.

Что сделано

Файл Домены
caddy/sites/apps.caddy gendsgn.ru, www.gendsgn.ru, meraocenka.ru, merahome.ru, meraotsenka.ru
caddy/sites/infra.caddy obsidian.gendsgn.ru, errors.gendsgn.ru, git.gendsgn.ru

Корневой Caddyfile ужался с 522 строк до 96: глобальные опции, snippet'ы, блок :80 и два import. Site-блоки перенесены дословно, вместе с комментариями — поведенческих правок нет, только перемещение.

Выбор режима — через import caddy/sites/{$CADDY_SITES:*}.caddy.

Test plan

caddy adapt в трёх режимах, домены посчитаны из полученного JSON:

  • без переменной8 доменов — текущее поведение Beget, не меняется ни на байт
  • CADDY_SITES=apps5: gendsgn.ru merahome.ru meraocenka.ru meraotsenka.ru www.gendsgn.ru
  • CADDY_SITES=infra3: errors.gendsgn.ru git.gendsgn.ru obsidian.gendsgn.ru

Две правки, найденные этой же валидацией (обе были бы тихими поломками на проде):

  1. Синтаксис дефолта у Caddy — {$VAR:default}, а не {$VAR:-default} как в shell/compose. С шелловской формой Caddy искал файл с литеральным именем caddy/sites/{$CADDY_SITES:-*}.caddy.
  2. import резолвится относительно импортирующего файла, поэтому import caddy/users.caddy.snippet при переносе внутрь caddy/sites/ перестал находиться → import ../users.caddy.snippet.

Известное ограничение

Пустое значение переменной — не то же самое, что незаданная: Caddy подставит пустую строку, получится путь caddy/sites/.caddy, конфиг не соберётся. Оставлено как есть осознанно — это ошибка оператора, которая падает громко (reload отказывает, старый конфиг продолжает работать), а не тихо отдаёт не тот набор доменов. Предупреждение стоит комментарием над монтированием в docker-compose.prod.yml.

Refs #3059, #3057, #2989

## Зачем Пункт 4 из чеклиста переезда. Корневой `Caddyfile` содержал все восемь доменов в одном файле, а деплой синхронизирует рабочее дерево с `origin/main` и перечитывает конфиг целиком. Значит на Selectel приехал бы файл со всеми восемью — и Caddy начал бы выпускать сертификаты для `obsidian` / `errors` / `git`, чей DNS указывает на Beget. ACME падал бы на HTTP-01, с риском упереться в rate limit Let's Encrypt. Вторая, более тяжёлая сторона той же проблемы (найдена при разведке порядка остановки/запуска, см. #3061): **сейчас этот Caddy — единственный публичный вход и для тех трёх доменов тоже.** Он уезжает, а Forgejo, GlitchTip и CouchDB остаются на Beget. Без разделения они отдавали бы 502, оставаясь при этом живыми — то есть отвалился бы и git, и приём ошибок, и синк волта, причём в момент переезда, когда они нужнее всего. Теперь на Beget остаётся Caddy с `CADDY_SITES=infra`. ## Что сделано | Файл | Домены | |---|---| | `caddy/sites/apps.caddy` | gendsgn.ru, www.gendsgn.ru, meraocenka.ru, merahome.ru, meraotsenka.ru | | `caddy/sites/infra.caddy` | obsidian.gendsgn.ru, errors.gendsgn.ru, git.gendsgn.ru | Корневой `Caddyfile` ужался с **522 строк до 96**: глобальные опции, snippet'ы, блок `:80` и два import. Site-блоки перенесены **дословно**, вместе с комментариями — поведенческих правок нет, только перемещение. Выбор режима — через `import caddy/sites/{$CADDY_SITES:*}.caddy`. ## Test plan `caddy adapt` в трёх режимах, домены посчитаны из полученного JSON: - [x] **без переменной** → **8** доменов — текущее поведение Beget, не меняется ни на байт - [x] `CADDY_SITES=apps` → **5**: gendsgn.ru merahome.ru meraocenka.ru meraotsenka.ru www.gendsgn.ru - [x] `CADDY_SITES=infra` → **3**: errors.gendsgn.ru git.gendsgn.ru obsidian.gendsgn.ru Две правки, найденные этой же валидацией (обе были бы тихими поломками на проде): 1. Синтаксис дефолта у Caddy — `{$VAR:default}`, а **не** `{$VAR:-default}` как в shell/compose. С шелловской формой Caddy искал файл с литеральным именем `caddy/sites/{$CADDY_SITES:-*}.caddy`. 2. `import` резолвится **относительно импортирующего файла**, поэтому `import caddy/users.caddy.snippet` при переносе внутрь `caddy/sites/` перестал находиться → `import ../users.caddy.snippet`. ## Известное ограничение Пустое значение переменной — не то же самое, что незаданная: Caddy подставит пустую строку, получится путь `caddy/sites/.caddy`, конфиг не соберётся. Оставлено как есть осознанно — это ошибка оператора, которая падает **громко** (reload отказывает, старый конфиг продолжает работать), а не тихо отдаёт не тот набор доменов. Предупреждение стоит комментарием над монтированием в `docker-compose.prod.yml`. Refs #3059, #3057, #2989
lekss361 added 1 commit 2026-08-23 21:10:44 +00:00
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
dacc8b410c
Корневой 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
bot-backend added 1 commit 2026-08-23 21:43:08 +00:00
test(mera): читать эффективный конфиг Caddy, а не только корневой файл
Some checks failed
CI Trade-In / changes (pull_request) Successful in 9s
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 10s
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 33s
87ffb5d621
CI поймал настоящую связь, которую разделение site-блоков порвало: два
фронтовых теста читают Caddyfile как ИСТОЧНИК ПРАВДЫ о публичном
периметре, а не дублируют список путей.

  public-perimeter.test.ts  — ищет блок meraocenka.ru, сверяет, что каждый
                              маршрут из PUBLIC_ROUTES раздаётся, и что у
                              каждой страницы есть 301 с длинного адреса
  RouteGuard.publicPaths.ts — вытаскивает матчер @meraPages и сверяет с
                              списком публичных путей в гварде

Оба комментария прямо объясняют, зачем так: списки уже расходились дважды
(15.08 и 16.08), причём страница в обоих случаях отдавала 200 и ломалась
только после гидратации — ни один тест по коду ответа этого не видел.

После переноса site-блоков в caddy/sites/*.caddy корневой файл этих блоков
не содержит, и проверки стали бы пустыми. Тесты честно упали на своих же
страховках («блок не найден — иначе проверки ниже пустые», «не нашли
матчер @meraPages — проверка стала бы пустой»), а не тихо позеленели.

Теперь оба читают эффективный конфиг: корневой Caddyfile + все
caddy/sites/*.caddy. Все файлы берутся намеренно — `import
caddy/sites/{$CADDY_SITES:*}` без переменной раскрывается в звёздочку,
то есть это и есть конфиг по умолчанию.

Проверено симуляцией ровно тех утверждений, на которых падал CI: срез
между meraocenka.ru и merahome.ru = 10 458 символов (тест ждёт >500),
матчер @meraPages найден, в нём 5 путей. Запустить vitest локально не
вышло — фронтовые зависимости не установлены (node_modules/.bin пуст),
так что окончательная проверка за CI.

Refs #3059
Author
Owner

CI поймал настоящую связь, которой я не ждал — и это хороший знак, что она поймалась громко.

Два фронтовых теста читают Caddyfile как источник правды о публичном периметре, а не дублируют список путей:

Тест Что берёт из конфига
public-perimeter.test.ts блок meraocenka.ru, сверка что каждый маршрут из PUBLIC_ROUTES раздаётся, и что у каждой страницы есть 301 с длинного адреса
RouteGuard.publicPaths.test.ts матчер @meraPages, сверка со списком публичных путей в гварде

Комментарии в обоих прямо объясняют, зачем так: списки уже расходились дважды (15.08 — на /, 16.08 — на /estimate), причём страница в обоих случаях отдавала 200 и ломалась только после гидратации, так что ни один тест по коду ответа этого не видел.

После переноса site-блоков в caddy/sites/*.caddy корневой файл их не содержит — и проверки стали бы пустыми. Тесты упали на своих же страховках («блок не найден — иначе проверки ниже пустые», «не нашли матчер @meraPages — проверка стала бы пустой»), а не тихо позеленели. Ровно то, ради чего эти страховки и писались.

Починено в 87ffb5d6: оба теста читают эффективный конфиг — корневой Caddyfile плюс все caddy/sites/*.caddy. Все файлы берутся намеренно: import caddy/sites/{$CADDY_SITES:*} без переменной раскрывается в звёздочку, то есть это и есть конфиг по умолчанию.

Проверено симуляцией ровно тех утверждений, на которых падал CI:

  • срез между meraocenka.ru { и merahome.ru {10 458 символов (тест ждёт >500)
  • @meraPages найден, в нём 5 путей

Запустить vitest локально не вышло — фронтовые зависимости не установлены (node_modules/.bin пуст), так что окончательная проверка за CI.

CI поймал настоящую связь, которой я не ждал — и это хороший знак, что она поймалась громко. Два фронтовых теста читают `Caddyfile` как **источник правды** о публичном периметре, а не дублируют список путей: | Тест | Что берёт из конфига | |---|---| | `public-perimeter.test.ts` | блок `meraocenka.ru`, сверка что каждый маршрут из `PUBLIC_ROUTES` раздаётся, и что у каждой страницы есть 301 с длинного адреса | | `RouteGuard.publicPaths.test.ts` | матчер `@meraPages`, сверка со списком публичных путей в гварде | Комментарии в обоих прямо объясняют, зачем так: списки уже расходились дважды (15.08 — на `/`, 16.08 — на `/estimate`), причём страница в обоих случаях отдавала 200 и ломалась только после гидратации, так что ни один тест по коду ответа этого не видел. После переноса site-блоков в `caddy/sites/*.caddy` корневой файл их не содержит — и проверки стали бы **пустыми**. Тесты упали на своих же страховках («блок не найден — иначе проверки ниже пустые», «не нашли матчер `@meraPages` — проверка стала бы пустой»), а не тихо позеленели. Ровно то, ради чего эти страховки и писались. **Починено в `87ffb5d6`:** оба теста читают эффективный конфиг — корневой `Caddyfile` плюс все `caddy/sites/*.caddy`. Все файлы берутся намеренно: `import caddy/sites/{$CADDY_SITES:*}` без переменной раскрывается в звёздочку, то есть это и есть конфиг по умолчанию. Проверено симуляцией ровно тех утверждений, на которых падал CI: - срез между `meraocenka.ru {` и `merahome.ru {` — **10 458** символов (тест ждёт >500) - `@meraPages` найден, в нём **5** путей Запустить vitest локально не вышло — фронтовые зависимости не установлены (`node_modules/.bin` пуст), так что окончательная проверка за CI.
bot-backend added 1 commit 2026-08-23 21:49:17 +00:00
fix(tests): восстановить экранирование \n в join — TS1002
All checks were successful
CI Trade-In / changes (pull_request) Successful in 8s
CI / backend-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / backend-tests (pull_request) Has been skipped
CI / changes (pull_request) Successful in 9s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m4s
dcf8c1655d
Мой предыдущий коммит попал в файлы через шелл-heredoc, который съел
экранирование: вместо двух символов backslash+n в литерал уехал НАСТОЯЩИЙ
перевод строки, и обе строки стали незакрытыми (TS1002 Unterminated
string literal). Именно поэтому frontend-checks остался красным, хотя сама
логика чтения объединённого конфига верна.

Refs #3059
lekss361 merged commit 60cb510ef2 into main 2026-08-23 21:52:48 +00:00
lekss361 deleted branch refactor/caddyfile-split-by-host 2026-08-23 21:52:49 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#3062
No description provided.