All checks were successful
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 / frontend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Successful in 1m56s
CI Trade-In / browser-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Has been skipped
CI / backend-tests (pull_request) Successful in 17m31s
Полный деплой ПТИЦЫ каждый раз делал `up -d --force-recreate --no-deps caddy`
и сносил единственный процесс, слушающий 80/443: замер 05.09 (#3274) — 67 с
`code=000` на ВСЕХ доменах хоста, включая публичный лендинг МЕРЫ. Заглушка
окна деплоя здесь бессильна по построению: её отдаёт тот же Caddy.
Что установлено, а не принято на веру:
* Безусловный флаг появился 17.05 (11e78d73) ради нового bind-маунта
`./preview` — «иначе новые volume mounts не появляются». Довод неверен:
`docker compose up -d` БЕЗ `--force-recreate` пересоздаёт контейнер сам при
смене описания сервиса или образа. Проверено на живом демоне (docker 28.4):
добавлен volume → новый id; тег переставлен на другой образ → новый id;
не менялось ничего → `Container … Running`, id тот же.
* Compose не видит только одного: СОДЕРЖИМОГО пофайлового bind-маунта.
`git reset --hard` пишет новый инод, контейнер держит прежний, и `caddy
reload` перечитывает старый текст (тот же механизм — Alertmanager 27.08 и
Alloy #3380). У Caddy так смонтированы пять путей: Caddyfile и четыре
сниппета; каталоги (caddy/sites, caddy/local, preview) этим не страдают —
самый частый случай, caddy/sites/apps.caddy, пересоздания НЕ требует.
* Отсюда же второй, беззвучный дефект: быстрый путь `deploy-caddy` делал голый
`exec caddy reload` после `git reset --hard`, то есть правка Caddyfile или
сниппета до контейнера не доезжала вовсе, а джоба уходила зелёной.
Оба пути деплоя теперь зовут ops/caddy-apply.sh: `caddy validate` одноразовым
контейнером по файлам С ХОСТА (битый конфиг не применяется и прокси не
трогает) → `up -d` без `--force-recreate` → если контейнер тот же, сверка
sha256 каждого пофайлового маунта с тем, что видит контейнер → пересоздание
ТОЛЬКО при расхождении, иначе `caddy reload` без разрыва соединений.
Не прочиталось — считаем расхождением: fail-safe в сторону прежнего поведения.
Гейт backend/tests/ops/test_3443_caddy_reload_not_recreate.py исполняет скрипт
с подставным `docker` и смотрит на совершённые действия, а не на его текст:
ничего не менялось → reload без пересоздания; правлен Caddyfile или любой из
сниппетов → пересоздание; правка в каталоге → без пересоздания; битый конфиг →
не тронуто ничего; compose пересоздал сам → второго пересоздания нет.
Отдельно — проводка в deploy.yml и запрет безусловного `--force-recreate` для
Caddy в полном деплое.
Приёмка (#3443) снимается ПОСЛЕ мержа, на живом деплое: непрерывная проба
`scripts/probe-deploy-window.sh` с хоста — максимальная серия `000` меньше 2 с
против нынешних 67 с.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
98 lines
7.3 KiB
Bash
Executable file
98 lines
7.3 KiB
Bash
Executable file
#!/bin/sh
|
||
# Применить текущий конфиг прокси к работающему Caddy (#3443).
|
||
#
|
||
# ЗАЧЕМ. Полный деплой ПТИЦЫ пересоздавал сам Caddy БЕЗУСЛОВНО
|
||
# (`up -d --force-recreate --no-deps caddy`), а вместе с контейнером исчезал
|
||
# единственный процесс, слушающий 80/443. Замер 05.09 (#3274): 67 с `code=000`
|
||
# на ВСЕХ доменах хоста — gendsgn.ru, meraocenka.ru и зеркала, включая
|
||
# публичный лендинг МЕРЫ. Это не 502/503: принимающего процесса нет вовсе,
|
||
# поэтому заглушка окна деплоя (caddy/sites/deploy-window.caddy.snippet) здесь
|
||
# бессильна по построению — её отдаёт тот же Caddy.
|
||
#
|
||
# ЧТО НА САМОМ ДЕЛЕ ТРЕБУЕТ ПЕРЕСОЗДАНИЯ. Безусловный флаг появился 17.05
|
||
# (11e78d73) ради нового bind-маунта `./preview` из docker-compose.prod.yml,
|
||
# который «не появлялся в running container». Довод неверен: `docker compose
|
||
# up -d` БЕЗ `--force-recreate` пересоздаёт контейнер сам, как только меняется
|
||
# описание сервиса или образ. Проверено на живом демоне (docker 28.4):
|
||
# добавлен volume → `Container … Starting/Started`, id контейнера новый;
|
||
# тег указан на др. образ → id новый;
|
||
# не менялось ничего → `Container … Running`, id тот же.
|
||
#
|
||
# Остаётся ровно один класс изменений, которого compose не видит: СОДЕРЖИМОЕ
|
||
# пофайлового bind-маунта. `git reset --hard` не правит файл на месте, а пишет
|
||
# новый инод; контейнер держит примонтированным прежний и продолжает читать
|
||
# его — `caddy reload` перечитает ровно тот же старый инод. Тот же механизм уже
|
||
# ловили на Alertmanager (27.08, deploy-metrics.yml) и на Alloy (#3380). У Caddy
|
||
# так смонтированы пять путей: Caddyfile и четыре сниппета. Каталоги
|
||
# (caddy/sites, caddy/local, preview) этим не страдают — правка внутри каталога
|
||
# видна контейнеру сразу, поэтому самый частый случай (caddy/sites/apps.caddy)
|
||
# пересоздания НЕ требует.
|
||
#
|
||
# ОТСЮДА ПОРЯДОК: проверить конфиг → `up -d` без `--force-recreate` → если
|
||
# контейнер остался тем же, сверить, видит ли он текущее содержимое пофайловых
|
||
# маунтов → пересоздать ТОЛЬКО при расхождении, иначе `caddy reload`, который
|
||
# не рвёт соединения.
|
||
#
|
||
# ГРАНИЦА. Сверка по СОДЕРЖИМОМУ, а не по иноду: файл, переписанный тем же
|
||
# текстом, пересоздания не требует. Не прочиталось (контейнер не запущен, в
|
||
# образе нет sha256sum) — считаем расхождением: fail-safe в сторону прежнего
|
||
# поведения, то есть пересоздания.
|
||
set -eu
|
||
|
||
# Оба вызывающих (job `deploy` и job `deploy-caddy` в .forgejo/workflows/deploy.yml)
|
||
# работают в /opt/gendesign, но не зависеть от cwd дешевле, чем это помнить.
|
||
cd "$(dirname "$0")/.."
|
||
|
||
COMPOSE="docker compose -p gendesign -f docker-compose.prod.yml"
|
||
|
||
caddy_cid() { $COMPOSE ps -aq caddy 2>/dev/null | tail -n1; }
|
||
|
||
# ── 1. Проверка ДО применения ────────────────────────────────────────────────
|
||
# Одноразовый контейнер читает файлы С ХОСТА — то есть ровно то, что поедет в
|
||
# работающий Caddy. `exec caddy validate` здесь не годится: он проверил бы
|
||
# старый инод, то есть предыдущую версию конфига. Образ и парсер те же, что на
|
||
# PR-гейте (ci.yml «Guard: Caddyfile синтаксически валиден»).
|
||
echo "→ проверяю конфиг прокси одноразовым контейнером…"
|
||
if ! docker run --rm -v "$PWD:/work:ro" -w /work caddy:2 \
|
||
caddy validate --config /work/Caddyfile --adapter caddyfile; then
|
||
echo "ОШИБКА: конфиг прокси не проходит проверку — НЕ применён."
|
||
echo " Работающий Caddy не тронут, домены живы. Чинить конфиг и повторять."
|
||
exit 1
|
||
fi
|
||
|
||
# ── 2. Описание сервиса и образ ──────────────────────────────────────────────
|
||
before=$(caddy_cid)
|
||
$COMPOSE up -d --no-deps caddy
|
||
after=$(caddy_cid)
|
||
if [ -z "$after" ]; then
|
||
echo 'ОШИБКА: после `up -d` контейнера caddy нет — смотри вывод compose выше.'
|
||
exit 1
|
||
fi
|
||
if [ "$before" != "$after" ]; then
|
||
echo "✓ Caddy пересоздан compose'ом: изменилось описание сервиса или образ (${before:-нет}→${after})."
|
||
exit 0
|
||
fi
|
||
|
||
# ── 3. Доехало ли содержимое пофайловых маунтов ──────────────────────────────
|
||
stale=$(docker inspect "$after" \
|
||
--format '{{range .Mounts}}{{if eq .Type "bind"}}{{.Source}}|{{.Destination}}{{println}}{{end}}{{end}}' \
|
||
| while IFS='|' read -r src dst; do
|
||
[ -n "${dst:-}" ] || continue
|
||
# Каталог инод не держит — пропускаем.
|
||
[ -f "$src" ] || continue
|
||
host_sum=$(sha256sum "$src" 2>/dev/null | cut -d' ' -f1)
|
||
seen_sum=$(docker exec "$after" sha256sum "$dst" 2>/dev/null | cut -d' ' -f1)
|
||
[ -n "$host_sum" ] && [ "$host_sum" = "${seen_sum:-НЕ_ПРОЧИТАНО}" ] || printf '%s ' "$dst"
|
||
done)
|
||
|
||
if [ -n "$stale" ]; then
|
||
echo "→ до контейнера НЕ доехали пофайловые маунты: $stale"
|
||
echo " (bind-маунт файла держит инод: reload перечитал бы старую версию — нужен recreate)"
|
||
$COMPOSE up -d --force-recreate --no-deps caddy
|
||
echo "✓ Caddy пересоздан — иначе правка осталась бы неприменённой."
|
||
exit 0
|
||
fi
|
||
|
||
# ── 4. Всё доехало — перезагрузка без разрыва соединений ─────────────────────
|
||
$COMPOSE exec -T caddy caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile
|
||
echo "✓ конфиг прокси перезагружен без пересоздания контейнера — окна недоступности нет."
|