#!/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 синтаксически валиден»). # # ГРАНИЦА ЭТОЙ ПРОВЕРКИ. Она обрывает применение до того, как конфиг попадёт в # работающий Caddy, — но только на быстром пути. В полном деплое ВЫШЕ по # скрипту (deploy.yml, `up -d $UP_SERVICES`) уже прошёл общий подъём всех # сервисов, и если правка одновременно ломает Caddyfile И меняет блок caddy в # docker-compose.prod.yml, контейнер пересоздастся там — с непроверенным # конфигом и раньше этой строки. Первая линия против этого — гейт на PR (#2913). echo "→ проверяю конфиг прокси одноразовым контейнером…" if ! docker run --rm -v "$PWD:/work:ro" -w /work caddy:2 \ caddy validate --config /work/Caddyfile --adapter caddyfile; then echo "ОШИБКА: конфиг прокси не применён — проверка не пройдена ЛИБО не удалось" echo " запустить проверочный контейнер (нет образа caddy:2, занят демон," 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. Доехало ли содержимое пофайловых маунтов ────────────────────────────── # Список маунтов читается ОТДЕЛЬНОЙ командой, а не в конвейере с циклом: в # `$(docker inspect … | while …)` статус подстановки — это статус `while`, то # есть всегда 0 (`pipefail` в POSIX-sh нет вовсе). Провал `docker inspect` # давал бы пустой список → «расхождений нет» → `caddy reload` → зелёная джоба с # надписью «окна недоступности нет», а прокси работал бы по СТАРОМУ конфигу. # Это ровно тот беззвучный отказ, ради которого написан весь скрипт. mounts=$(docker inspect "$after" \ --format '{{range .Mounts}}{{if eq .Type "bind"}}{{.Source}}|{{.Destination}}{{println}}{{end}}{{end}}') \ || mounts='' if [ -z "$mounts" ]; then echo "WARNING: список маунтов Caddy не прочитан (docker inspect молчит или упал) —" echo " сверить нечем, считаю расхождением: fail-safe в прежнее поведение." verdicts="" stale="(маунты не прочитаны)" else verdicts=$(printf '%s\n' "$mounts" | while IFS='|' read -r src dst; do [ -n "${dst:-}" ] || continue # Каталог инод не держит — пропускаем. Именно `-d`, а не `-f`: # отсутствующий/нечитаемый ФАЙЛ — не повод молча пропустить, в # контейнере в этот момент живёт старый инод со старым текстом. if [ -d "$src" ]; then continue; fi host_sum=$(sha256sum "$src" 2>/dev/null | cut -d' ' -f1) seen_sum=$(docker exec "$after" sha256sum "$dst" 2>/dev/null | cut -d' ' -f1) if [ -n "$host_sum" ] && [ "$host_sum" = "${seen_sum:-НЕ_ПРОЧИТАНО}" ]; then echo "OK $dst" else echo "STALE $dst" fi done) # «Сверили пять файлов» и «сверили ноль» обязаны различаться в логе — иначе # зелёная строка ниже одинаково означает и проверку, и её отсутствие. checked=$(printf '%s\n' "$verdicts" | grep -c . || true) echo "→ сверено пофайловых маунтов с тем, что видит контейнер: $checked" stale=$(printf '%s\n' "$verdicts" | sed -n 's/^STALE //p' | tr '\n' ' ') if [ "$checked" -eq 0 ]; then echo "WARNING: ни одного пофайлового bind-маунта не найдено — у Caddy их пять" echo " (Caddyfile + 4 сниппета). Считаю расхождением: fail-safe." stale="(пофайловых маунтов не найдено)" fi fi 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 "✓ конфиг прокси перезагружен без пересоздания контейнера — окна недоступности нет."