fix(deploy): Caddy пересоздаётся только когда правка иначе не доедет (#3443) #3506
No reviewers
Labels
No labels
Fable 5 ревью
GG-форсайт
admin
analytics
auth
automation
bug
business
chore
ci
compliance
data
data-moat
docs
duplicate
dx
enhancement
feedback/max
generative
needs-discussion
needs-human
observability
pause-bots
performance
priority/p0
priority/p1
priority/p2
priority/p3
scope/backend
scope/db
scope/devops
scope/frontend
scope/qa
scrapers
security
site-finder
stage/1
stage/2
status/blocked
status/done
status/needs-analysis
status/needs-fix
status/qa
status/ready
status/review
status/wip
tech-debt
tradein
ux
week ревью 1
wontfix
ИРД
вторичка
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: lekss361/gendesign#3506
Loading…
Add table
Reference in a new issue
No description provided.
Delete branch "fix/3443-caddy-selfdowntime"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #3443.
Полный деплой ПТИЦЫ пересоздавал сам Caddy безусловно и вместе с контейнером уносил единственный процесс, слушающий 80/443: 67 с
code=000на всех доменах хоста (замер 05.09 в #3274).Что требует пересоздания, а что нет — с уликами
1. Зачем вообще стоял
--force-recreate.git log -Sдаёт ровно один коммит —11e78d73от 17.05.2026, «force-recreate caddy чтобы новые compose mounts применились». В теле:reloadне пересоздаёт контейнер, поэтому новый bind-маунт./preview(PR #268) не появился в работающем контейнере. Довод не подтвердился.docker compose up -dБЕЗ флага пересоздаёт контейнер сам, как только меняется описание сервиса или образ — проверено на живом демоне, сервис с двумя маунтами:Оговорка про стенд: этот замер сделан на docker 28.4.0 / compose v2.39.4 (macOS), а прод — docker 29.7.2 / compose v5.5.0. Ревьюер подтвердил вывод на прод-комбинации: метка
com.docker.compose.config-hashживого контейнера совпадает сcompose config --hash=caddy, аup --helpна проде дословно говорит, что умолчание уже пересоздаёт контейнер при смене конфигурации или образа. Одну версию за другую не выдаём.2. Маунты Caddy по типу (
docker inspect gendesign-caddy-1, прод, 12.09):Caddyfilecaddy/users.caddy.snippetcaddy/metrics-ui.caddy.snippetcaddy/metrics-ingest.caddy.snippetcaddy/deploy-window.caddy.snippetcaddy/sitescaddy/localpreviewcaddy_data,caddy_config,caddy_logsgit reset --hardне правит файл на месте: проверено — инод 65870621 → 65870670. Пофайловый bind-маунт остаётся на прежнем иноде, иcaddy reloadперечитывает СТАРЫЙ текст (тот же механизм уже ловили на Alertmanager 27.08 и на Alloy #3380). Для каталогов этого нет — правка внутри видна контейнеру сразу.3. Что умеет
caddy reloadпо факту. Подхватывает всё, что лежит в КАТАЛОГАХ (caddy/sites/**— самый частый случай, 15 правокapps.caddyза 180 суток;caddy/local/**). НЕ подхватывает: смену образа, смену описания сервиса (маунты/env/порты) и содержимое пяти пофайловых маунтов.4. Побочно закрытая дыра — ещё НЕ сработавшая. Из п.3 следует, что быстрый путь
deploy-caddyс голымexec caddy reloadпослеgit reset --hardне доставил бы правкуCaddyfileили сниппета: на диске новая версия, прокси работает по старой, джоба зелёная. Важно не преувеличить: за 180 суток было 33 коммита «только конфиг прокси», из них 25 трогают пофайловый маунт, но быстрый путь был мёртв до сегодняшнего дня (#3448) — все 25 ехали полным деплоем и доезжали. Дыра открылась сегодня в 11:43Z вместе с мержем #3465 и ни разу не сработала. Честная формулировка: первый же caddy-only мерж потерял бы правку.Форма правки и почему такая
Оба пути деплоя зовут
ops/caddy-apply.sh; условие пересоздания вычисляемое, а не «всегда» и не «никогда»:caddy validateодноразовым контейнером по файлам с хоста (docker run --rm -v $PWD:/work:ro caddy:2) — тот же образец, что на PR-гейте ci.yml.exec caddy validateтут не годится: он проверил бы старый инод. Битый конфиг → скрипт падает, работающий Caddy не тронут.up -d --no-deps caddyбез--force-recreate— пересоздаст сам, если сменились образ/маунты/env/порты. Если пересоздал — на этом всё.sha256sumкаждого пофайлового bind-маунта (список берётся изdocker inspect, поэтому новый маунт в compose попадает в сверку автоматически) с тем, что видит контейнер. Число сверенных файлов печатается.--force-recreate(единственный способ доставить), с печатью того, какие именно пути не доехали. Нет расхождения →caddy reload, соединения не рвутся.Сверка по содержимому, а не по иноду: файл, переписанный тем же текстом, пересоздания не требует. Любой отказ самой сверки (
docker inspectмолчит/упал, ноль пофайловых маунтов, файла на хосте нет,docker execне ответил) считается расхождением — fail-safe в прежнее поведение.Граница проверки, которую правка НЕ закрывает: в полном деплое общий
up -d $UP_SERVICES(deploy.yml:959) поднимает в том числе caddy за ~110 строк до вызова скрипта. Правка, которая одновременно ломаетCaddyfileИ меняет блок caddy вdocker-compose.prod.yml, пересоздаст контейнер там — раньше валидации. Первая линия против этого остаётся прежней: гейтcaddy validateна PR (#2913). Записано в комментарии скрипта, чинить в этом PR не предлагается.Архитектурные варианты из issue (второй экземпляр Caddy с передачей сокета, socket activation) не реализованы: после разбора они нужны только для случая «содержимое пофайлового маунта изменилось», который стал редким. Следующий по дешевизне шаг, если и это окно мешает, — смонтировать
./caddyкаталогом вместо четырёх сниппетов пофайлово (заодно снимает класс #3102 «забыли маунт»); остался бы толькоCaddyfile. Не делаю здесь: трогает compose иscripts/check-caddy-snippet-mounts.py, а окно того не стоит без замера.Правки по итогам deep-ревью (коммит 2)
stale=$(docker inspect … | while …)подset -euбезpipefail(в POSIX-sh его нет) отдаёт статусwhile, то есть всегда 0: отказdocker inspectдавал пустой список → «всё доехало» →reload→ зелёная джоба с надписью «окна недоступности нет» при прокси на старом конфиге. Список теперь читается отдельной командой; провал, пустой вывод и «ноль пофайловых маунтов» считаются расхождением; в лог печатается число сверенных файлов.backend(ci.yml) не былоops/**, а все содержательные регрессии живут вops/caddy-apply.sh. Добавлена одна строка с объяснением — тот же класс, что #2950/#3448/#3467.[ -d "$src" ] && continueвместо[ -f "$src" ] || continue: пропуск по-fсклеивал «это каталог» и «файла на хосте нет» (а в контейнере в этот момент живёт старый инод).caddy validateбольше не называет причиной битый конфиг, когда упасть мог и сам запуск проверочного контейнера.Проверено
docker run --rm -v "$PWD:/work:ro" -w /work caddy:2 caddy validate …на дереве репозитория →Valid configuration. На проде условия те же:CADDY_SITESв.envне задан (дефолт*, оба site-файла), вcaddy/localтолько.gitignore.docker inspect --format '{{range .Mounts}}…'иdocker exec gendesign-caddy-1 sha256sum— исполнены на проде (чтение): шаблон отдаётsrc|dstровно по bind-маунтам,sha256sumвcaddy:2есть, и сегодня хост и контейнер совпадают по всем пяти файлам → на следующем деплое скрипт пошёл бы веткойreload.scripts/probe-deploy-window.sh, с хоста, 1555 образцов, шаг 0,54 с, окно без деплоя): 1555 × 200, максимальная серия не-200 — 0. Сам тракт пробы чист, и000в окне деплоя будет свойством деплоя, а не пробы.backend/tests/ops/test_3443_caddy_reload_not_recreate.py— 22 проверки, зелёные; соседниеtest_3448,test_2950,test_3029,test_3467— тоже.Мутационный прогон гейта (11 из 11 краснеют)
Дефект возвращался руками в рабочем дереве и убирался обратно копией файла (не стэшем — стэш общий на репозиторий).
if [ -n "$stale" ]→if true(пересоздавать всегда)test_nothing_changed_reloads_without_recreate,test_directory_mount_change_does_not_recreateif false(не пересоздавать никогда)test_changed_file_mount_forces_recreateи все четыре сниппетаup -d --force-recreate --no-deps caddyвернулся в jobdeploytest_full_deploy_has_no_unconditional_caddy_recreateexec caddy reloadtest_deploy_applies_caddy_config_through_the_script[deploy-caddy]docker inspectупал →stale=""(fail-open)test_unreadable_mount_list_falls_back_to_recreatetest_no_file_mounts_is_not_silence-dобратно на-f(исчезнувший файл молча пропущен)test_missing_host_file_is_not_skipped_as_a_directorycaddy reload … || truetest_failures_are_not_swallowedsh ops/caddy-apply.sh || trueв полном деплоеtest_failures_are_not_swallowedops/**убран из фильтраbackendв ci.ymltest_gate_runs_on_changes_to_the_script_itselftest_log_says_how_many_mounts_were_comparedКонтроль после восстановления файлов — 22 passed.
Приёмка — ПОСЛЕ мержа, не выполнена
Критерий записан до правки и проверяем только на живом деплое:
ssh poincare 'nohup /opt/gendesign/scripts/probe-deploy-window.sh https://meraocenka.ru/ 0.2 900 /tmp/probe-3443.tsv &') даёт максимальную серию000меньше 2 с против нынешних 67 с;deployвидно, какой веткой пошёл скрипт, и число сверенных маунтов («сверено пофайловых маунтов …: 5»);docker inspect gendesign-caddy-1 --format '{{.Created}}'НЕ меняется на деплое, который не трогал конфиг прокси. Оговорка:deploy.yml:794делаетcompose pullбез списка сервисов, поэтому свежий upstream-релизcaddy:2законно сменитCreatedи даст ложное «не сработало» — сверять вместе с логом Caddy (при пересоздании там холодный стартserving initial configuration, при reload — сообщение о перезагрузке конфига);Caddyfileбыстрым путём: содержимое обязано доехать (либо reload по совпавшим суммам, либо явное пересоздание с названными путями).Дата постановки приёмки — 12.09.2026; до её снятия считать правку непроверенной на проде.