Эксплуатация, резервные копии и восстановление
Оглавление · Жизненный цикл PostgreSQL · Миграции схемы · Наблюдаемость
Примеры production-команд ниже выполняются на VPS в Bash пользователем
atmanki-deploy или уполномоченным администратором. Нельзя заменить их локальным
docker compose и ожидать, что команда затронет сервер.
Удобная оболочка для текущего релиза
release_dir=$(readlink -f /opt/gheilt/current)
test -n "$release_dir" && test -f "$release_dir/release.env"
compose=(docker compose --project-name atmanki \
--env-file /opt/gheilt/.env --env-file "$release_dir/release.env" \
-f "$release_dir/compose.yaml" -f "$release_dir/compose.production.yaml")
dc() { "${compose[@]}" "$@"; }
Дальнейшие команды dc предполагают эту оболочку. Если current ещё нет,
используйте конкретный release directory из неудачного первого запуска.
Статус и логи
dc ps
dc logs --tail=100 web
dc logs --tail=100 strapi postgres
docker stats --no-stream
free -m
df -h
Логи могут содержать payload CMS. Не отправляйте их целиком в публичные issue.
docker stats помогает увидеть рост памяти, но не заменяет длительные метрики:
Prometheus/Grafana/alerting в проекте не настроены.
curl --fail https://gheilt.mxsource.xyz/api/health
curl --fail https://gheilt.mxsource.xyz/api/trpc/news.list
Аналитика и мониторинг
Для аналитики посещений, ошибок и мониторинга подготовлена self-hosted конфигурация Umami, GlitchTip, Prometheus и Grafana на том же VPS, что и приложения. Файлы в checkout не подтверждают выпуск или проверку сервисов на VPS; практическая глава описывает отдельные критерии этой проверки.
Для учебного проекта один VPS — осознанное упрощение. Мониторинг на нём помогает изучать метрики и разбирать сбои отдельных сервисов, пока сам мониторинг продолжает работать. При отказе всего VPS мониторинг также становится недоступен и не может отправить уведомление об этом отказе. Сбой сети или нехватка ресурсов хоста могут одновременно затронуть приложения и доставку уведомлений.
Независимую внешнюю проверку доступности и отдельный сервер мониторинга сейчас не добавляем: высокая доступность не является целью учебного проекта. Отсутствие уведомлений само по себе не подтверждает исправность сайта.
Применение новых настроек
Для изменения только кода используйте GitHub Actions. После изменения environment
нужно пересоздать контейнеры: restart не перечитывает environment.
dc up -d --no-build --pull never --wait web
Команда использует уже загруженные образы. Наличие отсутствующего образа — повод восстановить доступ к registry через штатный pipeline, а не собирать всё на VPS. Перезапуск CMS и базы должен учитывать активные операции и доступность приложений.
Что резервировать
| Объект | Зачем |
|---|---|
/opt/gheilt/.env | DB passwords, signing/encryption keys, token |
База strapi | Записи CMS и администраторы |
atmanki_s3_metadata | Каталог Garage: buckets, keys, объектные ссылки |
atmanki_s3_data | Байты S3 объектов |
atmanki_strapi_uploads | Прежние local uploads, на которые ещё может ссылаться CMS |
| PG globals | Роли; файл содержит чувствительные password hashes |
| Тома Caddy | TLS-ключи/конфигурация; можно перевыпустить, но сохранение полезно |
| Release metadata | Какая версия образов работала вместе с копией |
Репозиторий не содержит автоматического backup scheduler, offsite-хранилища или retention policy; одноразовый migration backup проверяет PostgreSQL/Garage restore. Копия на том же VPS не спасает от потери VPS. Храните шифрованную копию отдельно и проверяйте восстановление.
Пример согласованной копии в окно обслуживания
Это процедура с временной недоступностью CMS/очереди. Сначала отрепетируйте её
локально. Не выполняйте команды остановки автоматически ради чтения документации.
Далее предполагаются Bash, определённый dc и уже работающий production:
set -euo pipefail
umask 077
exec 9>/opt/gheilt/deploy.lock
flock -n 9
backup_dir="/opt/gheilt/backups/$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "$backup_dir"
# EXIT пытается вернуть сервисы и при ошибке копирования.
trap 'dc up -d --no-build --pull never --wait --wait-timeout 240' EXIT
dc stop --timeout 60 caddy web strapi s3
cp /opt/gheilt/.env "$backup_dir/environment.env"
cp "$release_dir/release.env" "$backup_dir/release.env"
printf '%s\n' "$release_dir" > "$backup_dir/release-path.txt"
dc exec -T postgres pg_dump -U atmanki -d strapi -Fc > "$backup_dir/strapi.dump"
dc exec -T postgres pg_dumpall -U atmanki --globals-only > "$backup_dir/globals.sql"
for volume in s3_metadata s3_data strapi_uploads caddy_data caddy_config; do
docker volume inspect "atmanki_$volume" >/dev/null
docker run --rm -v "atmanki_$volume:/source:ro" -v "$backup_dir:/backup" \
alpine:3.23 tar -czf "/backup/$volume.tgz" -C /source .
done
Проверьте exit status и наличие архивов. pg_dump берёт согласованный snapshot
каждой базы, но два отдельных dump не являются одним межбазовым snapshot.
Остановка приложений уменьшает расхождения; таймаут остановки не гарантирует
завершение произвольно долгой задачи. Caddy и Garage остановлены на время копирования: metadata SQLite и object files
копируются согласованно. Изменяемые файлы работающего Garage архивировать нельзя.
EXIT trap поднимает сервисы. После завершения процесса/скрипта освобождается lock.
Если выполняли пример интерактивно, выйдите из этого shell, чтобы освободить fd 9.
Затем проверьте доступность приложений и доставьте зашифрованную копию вне хоста.
Нельзя копировать активную папку PostgreSQL обычным tar вместо pg_dump.
Учебное восстановление базы без изменения production
На локальном Docker-стеке создайте новую базу с отдельным именем:
docker compose exec -T postgres createdb -U atmanki -O strapi learning_restore
docker compose exec -T postgres pg_restore -U atmanki --no-owner --role=strapi \
--exit-on-error --single-transaction -d learning_restore < /path/to/strapi.dump
docker compose exec -T postgres psql -U atmanki -d learning_restore -c '\dt'
/path/to/strapi.dump — доступная локальная учебная копия, не приватная production-
база. Если база уже существует, выберите другое имя. Не применяйте --clean к
рабочей базе для обхода конфликта. Проверка \dt доказывает только наличие таблиц;
нужна проверка данных и запуск отдельного экземпляра CMS на восстановленной базе.
Восстановление всего стека на новом изолированном хосте
Восстановите секреты и файлы релиза на изолированном хосте. Запустите только PostgreSQL, восстановите базу Strapi и оба тома Garage, затем запускайте CMS и сайт. Проверьте записи и изображения до переключения трафика.
# На новом хосте, когда текущий release и dc уже подготовлены:
dc up -d --no-build --pull never --wait postgres
dc exec -T postgres pg_restore -U atmanki --no-owner --role=strapi \
--exit-on-error --single-transaction -d strapi < "$backup_dir/strapi.dump"
docker volume create atmanki_strapi_uploads
docker run --rm -v atmanki_strapi_uploads:/restore -v "$backup_dir:/backup:ro" \
alpine:3.23 tar -xzf /backup/strapi_uploads.tgz -C /restore
Очистка контейнеров и образов стендов
Статус: очистка реализована в контроллере; выкладка и проверка на VPS — отдельный этап. Сохраняем полные SourceCraft-стеки; shared-хранилища и light/full для этой задачи не нужны. Цель очистки — освобождать место после обновления или удаления стенда, сохраняя возможность холодного запуска и предусмотренного отката без pull.
Что уже делает код
scripts/deploy/preview.sh remove проверяет состояние PR под deploy.lock,
удаляет маршруты, контейнеры, сеть и тома его Compose project. При отсутствии
current используются точные project labels. Затем удаляются каталог стенда
и power state. Перед удалением каталога образы попадают в отдельную очередь
/opt/gheilt/image-cleanup; следующий poll удаляет только свободные ссылки. Холодный режим выполняет stop, оставляя
контейнеры и их образы; пробуждение использует --pull never.
При обновлении Compose с --remove-orphans заменяет изменившиеся сервисы; неизменившиеся контейнеры
может сохранить. Старые release directories и образы остаются. Текущий обработчик
ошибки пытается вернуть предыдущие web/Storybook/docs, но не восстанавливает
Strapi и данные. Поэтому ошибка деплоя не является доказательством завершённого
отката: до подтверждения восстановления автоматическое удаление образов запрещено.
Сценарии и поведение
| Событие | Контейнеры | Образы и сохранённые поколения |
|---|---|---|
| Успешное обновление A → B | Compose заменяет изменившиеся сервисы; удаляем только доказанные orphan-контейнеры этого project | Сохраняем B и один предыдущий успешный набор A из четырёх digest |
| Следующее успешное обновление B → C | Проверяем точные labels и ID оставшихся контейнеров | Сохраняем C/B; A становится кандидатом, если нигде больше не нужен |
| Повтор доставки того же поколения | Не пересоздаём исправный стек ради уборки | Не создаём лишнюю запись предыдущей версии; повтор очистки идемпотентен |
| Неудачный pull или отказ по ресурсам | Рабочую версию сохраняем | Сохраняем образы незавершённого кандидата до завершения или явного отказа от попытки |
| Неудачный первый deploy | Останавливаем доказанные частичные application-контейнеры; сохраняем данные для повтора | Образы закреплены за незавершённой попыткой; полное удаление выполняется при закрытии/TTL |
| Неудачное обновление и откат | Закрываем доступ; останавливаем CMS кандидата. Возврат web/Storybook/docs остаётся частичным; данные/CMS проверяются отдельно | Текущий и pending-наборы защищены; новый успешный deploy завершает попытку. Ошибка восстановления оставляет образы для диагностики |
| PR закрыт или истёк TTL | Удаляем только этот project, его маршруты и данные по существующему контракту | Все известные поколения этого PR становятся кандидатами после удаления контейнеров |
| Новый push во время уборки | Под lock повторно проверяем provider/head/generation; устаревшая уборка прекращается | Перед каждым удалением заново проверяем актуальные ссылки |
| Холодный стенд или LRU-охлаждение | Только stop; контейнеры сохраняются | Текущий и предусмотренный предыдущий наборы остаются локально |
| Один образ нужен нескольким стендам | Чужие контейнеры сохраняются, в том числе остановленные | Образ остаётся, пока на него ссылается хотя бы один защищённый набор или контейнер |
| Смена release-линии staging | Обновляем существующий стек с сохранением редакторских данных | Прежнее поколение защищаем как предыдущий успешный набор, даже при одинаковом SHA |
| Production blue-green | Активный и сохранённый резервный цвет исключаем из уборки PR/test/staging | Образы production и незавершённого promotion/recovery всегда защищены |
| Сбой уборки после удаления PR | Не повторяем удаление по одному имени без проверки актуального поколения | Закрытая запись кандидатов сохраняется отдельно от каталога PR; следующий poll повторяет только оставшиеся операции |
| Неизвестные labels, manifest, image ID или повреждённое состояние | Не удаляем неизвестные контейнеры | Автоматическая очистка пропускается с диагностикой; требуется аудит |
«Предыдущий набор» закрепляет только head и image digests. Старые release directories не очищаются, но повторная сборка того же SHA может перезаписать его release.env. Журнал не является готовой копией всей прежней конфигурации. Это не копия базы и медиа и не гарантия совместимости старой CMS с изменённой схемой. Не добавляем автоматический rollback данных ради очистки. Сохранение набора относится к поколению и digest, а не только к SHA: повторная сборка того же commit может получить другие образы.
Как отличать свободный образ от нужного
Кандидаты ограничены четырьмя application images, явно записанными lifecycle этого проекта. PostgreSQL, Garage, Caddy, observability, backup/restore helpers, build cache и неизвестные локальные образы исключены. Перед удалением собираем ссылки текущих и предыдущих успешных поколений всех стендов, pending attempts, production active/recovery и всех контейнеров Docker, включая остановленные. Ссылки на digest сопоставляем с локальными image ID: разные ссылки могут вести на один и тот же образ. При неоднозначности сохраняем весь образ.
Защиту кандидата записываем до первого pull: сейчас receive.pull_images
вызывается раньше locked deploy, и одна блокировка уборки без учёта скачивания
не предотвращает удаление нового образа между pull и запуском. Запись завершённого
успеха и выбор предыдущего набора выполняются только после smoke и ready.
Если HTTP smoke прошёл, но запись ready прервалась, попытка остаётся защищённой.
Перед удалением каталога PR сохраняем его кандидаты в закрытую очередь с
id и lastRemoved head/generation; pending/current содержат head и полный набор digest. Применение использует общий deploy.lock, повторную
проверку ссылок и точные ID. Контейнеры убираются раньше образов. Ошибка очистки
фиксируется отдельно от результата деплоя; очередь позволяет повторить уборку
следующим циклом существующего контроллера. Нехватка места не отменяет защиту
холодных стендов или rollback-набора.
При первой установке журналов уже работающий стенд сохраняет всю известную историю образов: manifests не доказывают, какое прежнее поколение прошло smoke. Первое новое успешное обновление записывает предыдущим доказанный ready-набор и разрешает обычную уборку более старых ссылок. Неудачная первая попытка обновления эту защиту не снимает.
Глобальные docker system prune, container prune и image prune -a не подходят:
Docker не знает о сохранённых release manifests. Удаление делается адресно через
docker image rm <repository@digest> без --force. В dry-run отчёте перечисляются
точные кандидаты, защищающие ссылки и причины отказа без env и секретов.
Общие слои могут оставаться после удаления ссылки; освобождённое место измеряется,
а не вычисляется суммированием размеров образов. Registry retention — отдельная
задача: локальное удаление образа не удаляет его из YC Registry.
Поведение image rm,
границы image prune.
Что проверить до включения
Контрактные тесты должны воспроизвести все строки таблицы: особенно холодный
стенд без доступа к registry, одинаковый image ID у разных digest, обновление
head во время уборки, повтор одной попытки, частичный pull, ошибку отката и
повреждённое состояние. В изолированном CI нужны два полноценных стенда с общим
application image, обновления A → B → C, принудительная ошибка обновления,
холодный запуск с --pull never, удаление одного PR и повтор уборки после сбоя.
Сверяем точные container/image ID и сохранность второго стенда и его volumes.
Живая приёмка сначала выполняется на отдельном проверочном PR; глобальная уборка
на VPS в эту проверку не входит.
Production → testing: процедура обновления данных
При импорте snapshot оригинальные медиа не оптимизируются повторно: Strapi обычно перекодирует изображения при загрузке, что меняет байты и SHA-256. Исключение действует только внутри отдельного процесса импорта; обычная загрузка через CMS сохраняет свои настройки.
Статус: реализована отдельная команда reset с dry-run и восстановлением.
Перед переносом живых данных обязательны успешный контейнерный drill точного
commit и проверка на VPS. Код и CI не заменяют подтверждение живого переноса.
Здесь testing — окружение test. Staging и PR не обновляются этой операцией.
Используем уже принятый published snapshot текущей production CMS/Garage:
опубликованные документы, их связи, используемые файлы и форматы изображений.
Это перенос контента приложения, а не PostgreSQL streaming replication.
Черновики, администраторы, сессии, API-токены, webhook, production-пароли и
неиспользуемые S3-объекты в test не переносятся. Полный clone production БД —
отдельная задача с проверкой закрытых данных и доступов, а не режим этого импортера.
Что уже работает и где граница
При первом запуске preview.sh prepare выбирает CMS из production active.json,
экспортирует опубликованный контент и затем импортирует его в пустой выделенный
стек. Обычные обновления кода сохраняют test-контент. Импортер хранит checksum
снимка и прогресс, позволяет повторить тот же импорт после прерывания и
отказывается принимать другой снимок поверх существующего контента.
Удаление файла imported не превращает этот путь в обновление данных.
Exporter сравнивает несколько чтений документов, в том числе после скачивания media, и отклоняет обнаруженные изменения источника. Файлы проверяются по SHA-256, размеру и ссылкам; модели — по fingerprint. Это защита от обнаруживаемых изменений, не атомарный snapshot общей транзакции PostgreSQL/S3. Для воспроизводимого переноса согласуйте окно без редакторских изменений и удалений media; чтение production сайта продолжает работать. Нестабильный источник требует нового экспорта.
Политика тестовых правок
| Режим | Результат | Поддержка сейчас |
|---|---|---|
| Сохранить редакторские правки | Существующий test продолжает работать; новый снимок не накладывается поверх него | Отказ при конфликте уже реализован; merge новых production-правок не реализован |
| Заменить контент test | После backup test получает новый опубликованный production-контент; прежние тестовые правки остаются только в backup/сохранённых томах | Команда test-refresh.py --mode reset --apply; требуется согласовать замену test |
Режим должен быть явно указан перед запуском. Reset не запускается по расписанию, при push, обновлении образов или повторе CI. Согласование режима не означает разрешения удалить текущие тестовые данные без готового backup и проверки отката.
Порядок для reset
- Зафиксировать источник и цель. Под общим deploy lock проверить
SourceCraft provider,
id=test, текущие head/generation, production active receipt и доступные RAM/диск. Записать image digests, пути конфигурации и точные ID томов test. Источник выбирается из active receipt, не по предположению о blue/green. Тестовые образы не заменяются production-образами. - Получить новый неизменяемый снимок. Экспортировать в отдельный закрытый
каталог, сохранив production receipt, время, checksum manifest и количества
документов/файлов. Read-only production token используется только exporter;
его временный env удаляется до запуска target importer. Снимок не попадает
в публичный CI artifact. Нельзя переиспользовать старый
snapshotтолько потому, что в нём уже есть manifest. - Проверить до изменения test. Проверить файлы и fingerprint моделями
текущего test Strapi image. CLI
node scripts/import-published.mjs --snapshot /snapshot --dry-runпроверяет сам снимок без загрузки target CMS; он не проверяет владельца test, конфликты живой БД, ресурсы или план reset. Эти проверки нужны отдельно. Несовместимые модели требуют отдельного изменения схемы; процедура не продвигает новые образы автоматически. - Закрыть записи и пробуждение test. Остановить обслуживаемые test-приложения, закрыть его маршруты на время обслуживания и запретить HTTP wake/CI deploy этого поколения. Повторно сверить lease под lock. Production, другие PR, staging и их данные продолжают работать. Простого удаления контейнера недостаточно: обычный runtime может попытаться поднять его снова.
- Сохранить восстановимый test. Сохранить архивы всех пяти тестовых томов, dump БД из восстановленной копии, test env, исходный state, указатель data-volumes и image digests. Current, routes и существующий import marker эта операция не заменяет; конфигурация текущего релиза остаётся в его каталоге. Проверить restore в изолированной БД/хранилище, затем удалить только scratch ресурсы проверки. Backup включает тестовые черновики и пользователей; права каталога 0700, файлов 0600. При отказе backup восстановить старый test и завершить операцию до reset. Сохранённый backup закрепляет образы от уборки.
- Импортировать в пустое тестовое хранение. Подготовить новые выделенные тома этого test, сохранив старые тома для отката. Target importer получает только test DB/S3 credentials и read-only mount нового снимка. Тестовые signing keys и настройки доступа не копируются с production. Provisioning создаёт собственные test admin/token/webhook; URLs и связанные media переписываются на test. Только после успешного импорта обновить его markers.
- Проверить и открыть test. Сверить полный отчёт импорта с manifest,
опубликованные коллекции, Site и связи; ID target могут отличаться от source.
Проверить отсутствие ссылок на production CMS/media, Content API read-only,
/api/healthс прежним test SHA,/api/content-health, CMS, Storybook/docs и media Range. Сбросить кеш web штатной ревалидацией. Затем сохранить новый generation/state и receipt обновления данных, восстановить маршруты и cold lifecycle. Снимок описывает состояние на время экспорта, не текущую копию production в реальном времени. - При сбое вернуть весь прежний test. Остановить новые контейнеры, вернуть старые тома/config/env/markers/current/routes и проверить прежний контент, CMS и web до открытия маршрутов. Не смешивать старую БД с новыми media или наоборот. Если восстановление не удалось, сохранить закрытые маршруты, обе версии и журнал для оператора. Незавершённая попытка и её образы защищены от уборки; повтор использует тот же проверенный снимок и сохранённый прогресс.
Перед применением нужен dry-run отчёт с источником, целью, поколением, числами документов/media, результатом проверки моделей и списком заменяемых test-ресурсов. В отчёте нет токенов, паролей и содержимого документов. Законченный перенос сохраняет snapshot checksum, backup path, прежние/новые resource ID и результаты проверок; старые тома не удаляются автоматически вместе с application images.
Команды и подключение
Команды выполняет deploy-пользователь после установки проверенных trusted tools:
tools=$(readlink -e /opt/gheilt/preview-runtime/current)
# Только план: Docker и журналы не меняются.
python3 "$tools/scripts/deploy/environment-cleanup.py" collect
# Адресное применение того же алгоритма под deploy.lock.
python3 "$tools/scripts/deploy/environment-cleanup.py" collect --apply
Отчёт содержит digest, причину protected, container или unused и
protectedBy — защищающие ссылки на тот же image ID. unused может означать,
что ссылка уже отсутствует локально: повтор в этом случае завершает очередь.
Контроллер вызывает сборку мусора после обработки стендов; ошибка уборки
не отменяет успешный deploy. Отдельный standalone receiver тоже закрепляет
candidate до pull и требует установленные trusted lifecycle tools.
Команды оператора
Запускайте из того же immutable каталога tools, которым обслуживается runtime.
Перед первым reset обновите работающий dispatcher: проверка /capabilities на
его Unix socket не позволит старому процессу запустить reset без поддержки новых
томов и блокировки запросов. Обычный deploy не вызывает эту команду.
tools=$(readlink -e /opt/gheilt/preview-runtime/current)
# Экспорт и проверка нового published snapshot; test продолжает работать.
python3 "$tools/scripts/deploy/test-refresh.py"
# После согласования замены редакторских данных test:
python3 "$tools/scripts/deploy/test-refresh.py" --mode reset --apply
# Если операция прервана, восстановление запускается отдельно:
python3 "$tools/scripts/deploy/test-refresh.py" --recover --apply
Можно передать заранее проверенный закрытый каталог через --snapshot /absolute/path.
Режим preserve отказывается изменять test: автоматического merge контента нет.
Dry-run никогда не запускает recovery; при незавершённой операции он останавливается.
Операция держит /opt/gheilt/deploy.lock. Это также временно задерживает lifecycle
других стендов и promotion, хотя работающие контейнеры продолжают обслуживать
запросы. Журнал /opt/gheilt/test-refresh/intent.json дополнительно закрывает test
для HTTP и новых deploy после падения процесса. Backup сохраняет все пять backend
томов, env и исходный state. Проверяются извлечённые деревья, запуск PostgreSQL с
чтением таблиц/dump и запуск Garage; если в прежнем test есть media, сравнивается
один реальный объект. При пустой библиотеке проверяется запуск и bucket.
Новые тома закрепляются в /opt/gheilt/previews/test/data-volumes.json; эту
настройку используют дальнейшие deploy и холодное пробуждение. После импорта
проверяются опубликованные документы/связи, read-only token и все snapshot-файлы,
включая Range. Compose healthchecks проверяют CMS/Storybook/docs, web проверяет
SHA и content-health; кеш сбрасывается через штатный webhook. Маршруты не
пересоздаются: до удаления intent dispatcher отвечает 503. После открытия
выполните публичный smoke по доменам test — внутренние проверки не подтверждают
TLS и весь путь через Caddy.
При сбое до commit возвращаются старые тома/env/state. Если state уже переключён на записанный новый generation/checksum, recovery проверяет и завершает новое поколение, а не затирает его. Неудачный recovery оставляет intent и закрытый test. Старые и неудачные новые тома остаются для аудита; эта задача автоматически удаляет только application images, а не сохранённые данные.
Обязательные проверки процедуры
Первое наполнение пустого test; повтор того же снимка; новый снимок при наличии
test-правок в обоих режимах; публикация/удаление source media во время экспорта;
несовместимые модели; повреждённый или неполный snapshot; нехватка места до reset;
ошибка импорта после части документов/media; отказ ревалидации или открытия
маршрутов; прерывание после переключения до receipt; попытка wake/deploy/cleanup
во время reset. Контрактные тесты проверяют порядок backup/import/commit, dry-run, восстановление
и блокировку wake; это не заменяет контейнерный drill. После каждого искусственного сбоя проверяются прежний test,
его drafts/media и неизменность production и соседнего PR. SourceCraft check-task/test-data-drill запускает
scripts/tests/integration_test_refresh.py: настоящие PostgreSQL, Garage и Strapi,
фиктивные HTTP companions, успешный reset, отказ после импорта и холодный запуск.
У него нет production secrets или данных; локальные production-сборки запрещены.
Этот drill не подтверждает TLS/Caddy и поведение настоящего Next.js — публичный
smoke остаётся отдельным шагом.
Код процедуры: scripts/deploy/test-refresh.py, scripts/deploy/preview-data.py,
infra/strapi/scripts/verify-published.mjs; существующие части: scripts/deploy/preview-source.py,
infra/strapi/scripts/lib/published-snapshot.mjs,
infra/strapi/scripts/lib/import-published.mjs.
Ручной откат образов
Для отката только web/Caddy на VPS в Bash выберите существующий release. CMS и S3 сохраняются; их откат выполняется отдельно после проверки данных:
rollback_dir=/opt/gheilt/releases/REPLACE_WITH_COMMIT_SHA
old_compose=(docker compose --project-name atmanki \
--env-file /opt/gheilt/.env --env-file "$rollback_dir/release.env" \
-f "$rollback_dir/compose.yaml" -f "$rollback_dir/compose.production.yaml")
exec 9>/opt/gheilt/deploy.lock
flock -n 9
# Use the current renderer even when the selected release predates monitoring.
fragment=/opt/gheilt/observability/current/infra/observability/proxy.caddy
if [[ -f "$fragment" ]]; then
python3 /opt/gheilt/observability/current/scripts/deploy/render-caddy.py --replace-observability \
"$rollback_dir/infra/Caddyfile.production" "$fragment" > "$rollback_dir/Caddyfile.combined"
cat "$rollback_dir/Caddyfile.combined" > "$rollback_dir/infra/Caddyfile.production"
fi
"${old_compose[@]}" config --quiet
mapfile -t images < <("${old_compose[@]}" config --images)
docker image inspect "${images[@]}" >/dev/null
"${old_compose[@]}" up -d --no-deps --no-build --pull never --wait --wait-timeout 240 web caddy
curl --fail https://gheilt.mxsource.xyz/api/health
if [[ -f "$fragment" ]]; then
curl --fail https://analytics.gheilt.mxsource.xyz/api/heartbeat
curl --fail https://errors.gheilt.mxsource.xyz/_health/
curl --fail https://monitoring.gheilt.mxsource.xyz/api/health
fi
ln -sfn "$rollback_dir" /opt/gheilt/current.next
mv -Tf /opt/gheilt/current.next /opt/gheilt/current
Сначала замените SHA, проверьте совместимость и наличие образов. Это не откат базы. Для постоянного исправления измените Git/branch: следующий push main снова применит содержимое main. Не воспринимайте удачный health как полную проверку старого релиза.
Обновление зависимостей и освобождение диска
Обновляйте небольшими группами: workspace через pnpm, CMS через её npm lockfile,
инфраструктуру через image tags. При major-обновлении PG нужен план миграции данных;
замена 17-alpine на новый major поверх старого volume не является таким планом.
Сначала df -h, docker system df и список релизов. Не запускайте глобальный prune
с volumes: можно потерять данные и образы для rollback. Для известных application
digests реализована адресная очистка, описанная выше. Retention резервных копий,
сохранённых томов и образов в registry автоматически не выполняется.
Дополнение для Garage
Прежний пример backup покрывает PostgreSQL и local uploads, но после добавления
S3 его недостаточно: отдельно нужны объекты atmanki_s3_data и metadata
atmanki_s3_metadata, а также ключи из .env. Для согласованной cold-копии
остановите Strapi и Garage; на это время media недоступны. Скопируйте оба тома,
затем поднимите Garage, дождитесь health и поднимите CMS. Проверьте восстановление
на отдельном экземпляре. Не снимайте работающую SQLite metadata обычным tar и
не считайте копию на том же VPS защитой от потери VPS.
Проверяемая предмиграционная копия
release.sh вызывает scripts/deploy/backup-content.sh один раз перед первой новой CMS под deploy flock. Он сохраняет SQL, оба Garage volumes, old uploads, env и release refs в каталог 0700, восстанавливает SQL в отдельную scratch DB и Garage в отдельную сеть с отдельными volumes. Cleanup удаляет только созданные scratch объекты и возвращает прежние приложения. Маркер /opt/gheilt/content-migration.backup указывает на копию. Это проверка миграции, не планировщик ежедневных backups и не offsite backup.
Ручной повтор выполняйте в согласованное окно обслуживания с deploy lock. Новую CMS/data не откатывайте запуском старого Strapi image: используйте проверенную копию на изолированном стенде и отдельный план восстановления. Rollback web использует --no-deps и сохраняет CMS/S3. Не удаляйте volumes исходного проекта ради restore drill.
current хранит релиз web, cms-current — фактически работающий релиз CMS. Во время
подготовки и частичного rollback они могут различаться; backup сохраняет обе ссылки
и возвращает каждую компоненту по её собственной версии. Garage restore drill
также читает контрольный объект и сравнивает байты, а не только проверяет bucket info.
Эксплуатация стека наблюдаемости
Конфигурация находится в infra/observability; до первого выпуска ограничения
из раздела о планируемом мониторинге сохраняются. После выпуска используется
отдельный project, который можно диагностировать без пересоздания приложений:
obs_release=$(readlink -f /opt/gheilt/observability/current)
obs=(docker compose --project-name atmanki-observability \
--env-file /opt/gheilt/observability/.env \
--env-file "$obs_release/infra/observability/images.env" \
-f "$obs_release/infra/observability/compose.yaml")
"${obs[@]}" ps
docker stats --no-stream
Секреты не выводите через docker inspect environment или compose config.
Для проверки синтаксиса используйте config --quiet.
В резервные копии добавьте базы umami и glitchtip, секреты observability,
atmanki-observability_grafana_data (аккаунты и настройки Grafana) и
atmanki-observability_glitchtip_uploads (артефакты source maps).
Изменяемую SQLite-базу Grafana копируют согласованно с остановкой её контейнера
либо штатным способом backup, а не произвольным tar работающего volume.
После определения dc из начала главы можно получить отдельные SQL snapshots:
dc exec -T postgres pg_dump -U atmanki -d umami -Fc > "$backup_dir/umami.dump"
dc exec -T postgres pg_dump -U atmanki -d glitchtip -Fc > "$backup_dir/glitchtip.dump"
backup_dir должен быть заранее подготовленным защищённым каталогом.
Два dump — два snapshots, а не общая транзакция. Резервируйте роли вместе с
остальными PostgreSQL globals и проверяйте restore на изолированном экземпляре.
Prometheus history — временные измерения, она не нужна для повторного запуска
метрик; её потеря оставляет разрыв на графике, а не повод показывать нули.
Проверяемый backup для будущего blue-green
scripts/deploy/production-backup.py — отдельный путь под deploy.lock. Он сверяет
реальные backend mounts, останавливает только CMS/S3 писателей и сохраняет
PostgreSQL dump, Garage metadata/data и uploads. Caddy не пересоздаётся. Restore
проверяется в scratch database по counts всех текущих public tables и в отдельных
Garage volumes/network по чтению контрольного объекта. Старое имя таблицы news
не используется как предположение о текущих моделях.
Private backup (directory700/files600) остаётся в /opt/gheilt/backups.
verified.json появляется только после restore и восстановления здоровья
исходных писателей. Durable production/backup-intent.json позволяет вернуть
контейнеры по прежним ID после прерывания, без старого Compose. При ошибке cleanup
intent и backup сохраняются; production backup не отправляется CI.
Скрипт пока проверен контрактными тестами в рабочей ветке; живой restore drill
входит в приёмку релизной доставки. Копия на этом VPS не заменяет внешнюю backup.
Restore drill также извлекает strapi_uploads.tar в отдельный временный volume.
Дерево сверяется по типам, правам, владельцам, link targets и хешам файлов;
повреждённый архив или расхождение запрещает verified receipt. Volume включён
в crash journal и удаляется при восстановлении.