Эксплуатация, резервные копии и восстановление
Оглавление · Далее: проверки
Примеры production-команд ниже выполняются на VPS в Bash пользователем
gheilt-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, на том же VPS, что и приложения. Конкретные инструменты пока обсуждаются; этот раздел фиксирует решение о размещении, а не подключённые сервисы.
Для учебного проекта один VPS — осознанное упрощение. Мониторинг на нём помогает изучать метрики и разбирать сбои отдельных сервисов, пока сам мониторинг продолжает работать. При отказе всего VPS мониторинг также становится недоступен и не может отправить уведомление об этом отказе. Сбой сети или нехватка ресурсов хоста могут одновременно затронуть приложения и доставку уведомлений.
Независимую внешнюю проверку доступности и отдельный сервер мониторинга сейчас не добавляем: высокая доступность не является целью учебного проекта. Отсутствие уведомлений само по себе не подтверждает исправность сайта.
Применение новых настроек
Для изменения только кода используйте GitHub Actions. После изменения environment
нужно пересоздать контейнеры: restart не перечитывает environment.
dc up -d --no-build --pull never --wait web
Команда использует уже загруженные образы. Наличие отсутствующего образа — повод восстановить доступ к registry через штатный pipeline, а не собирать всё на VPS. Перезапуск очереди/баз должен учитывать активные задачи и доступность приложений.
Что резервировать
| Объект | Зачем |
|---|---|
/opt/gheilt/.env | DB passwords, signing/encryption keys, token |
База strapi | Записи CMS и администраторы |
atmanki_s3_metadata | Каталог Garage: buckets, keys, объектные ссылки |
atmanki_s3_data | Байты S3 объектов |
atmanki_strapi_uploads | Media-файлы, на которые ссылается 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 run --rm -v "gheilt_$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
Ручной откат образов
Для отката только 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. Retention/cleanup в проекте
пока не автоматизированы.
Дополнение для 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 — временные измерения, она не нужна для повторного запуска
метрик; её потеря оставляет разрыв на графике, а не повод показывать нули.