Учебное руководство
Главы руководства
На этой странице

GitHub Actions и развёртывание на VPS

Оглавление · Далее: эксплуатация

Текущая схема

GitHub repository — maderwin/gheilt, deployment branch — main. Сервисы находятся на VPS gheilt.mxsource.xyz; его поддомены должны указывать туда же. Образы CI собирает для linux/amd64. Перенос на ARM-хост требует изменения platforms.

ДоменСервис
gheilt.mxsource.xyzNext.js
cms.gheilt.mxsource.xyzStrapi с собственным входом
hatchet.gheilt.mxsource.xyzHatchet HTTP с собственным входом

На VPS нужны Docker, Compose, Python 3, curl и стандартные Linux-утилиты flock, install, readlink, mv. Сборка npm/pnpm на VPS не выполняется. Поддерживаемая bootstrap-среда — Ubuntu/Debian с systemd и sudo-доступом.

Контент для статической сборки

Web-образ требует доступ к опубликованному контенту CMS во время сборки:

  • GitHub variable STRAPI_BUILD_URL — HTTPS URL CMS, доступный runner.
  • GitHub variable CMS_MEDIA_PUBLIC_URL — публичный URL media (для проверки origin).
  • GitHub secret STRAPI_READ_TOKEN — read-only API token CMS.

Docker передаёт token через BuildKit secret strapi_read_token; он не записывается в ARG/ENV образа. CMS_BUILD_ID в CI равен SHA релиза и заставляет заново читать CMS при новой сборке, даже если другие Docker-слои остались в кеше. Runtime по-прежнему использует настройки VPS .env и внутренний адрес http://strapi:1337. Для локальной Docker-сборки STRAPI_BUILD_URL задаётся в .env; CMS нужно предварительно запустить и наполнить. В Linux CMS должна быть доступна по адресу, который видит builder (loopback хоста может быть недоступен). Сборка без CMS или опубликованного Site завершается ошибкой.

CI проверки используют изолированную CMS через scripts/check-static-site.mjs; боевые token и контент не передаются в проверки pull request.

Первичная подготовка нового хоста

Для уже подготовленного VPS не повторяйте bootstrap ради обычного обновления. Административный доступ и deployment key — разные полномочия.

Создание отдельного ключа в рабочей копии (не перезаписывайте существующий):

install -d -m 700 .local
ssh-keygen -t ed25519 -N '' -C gheilt-github-actions -f .local/deploy_ed25519

После проверки identity сервера загрузите bootstrap и передайте публичный ключ в stdin. Шаблон использует начального администратора текущего проекта:

scp -i ~/.ssh/id_maderwin.dev scripts/deploy/bootstrap.sh \
  maderwin@gheilt.mxsource.xyz:/tmp/gheilt-bootstrap.sh
ssh -i ~/.ssh/id_maderwin.dev maderwin@gheilt.mxsource.xyz \
  'sudo -n bash /tmp/gheilt-bootstrap.sh' < .local/deploy_ed25519.pub

Этот пользователь/ключ должен принадлежать владельцу сервера; для другой среды подставьте собственный административный доступ. Скрипт устанавливает Docker из официального apt repository, создаёт gheilt-deploy, добавляет его в docker group, настраивает authorized_keys и /opt/gheilt/releases.

Для deployment key запрещены forwarding и PTY. Однако docker group всё равно даёт административные возможности на хосте. Это не полноценный restricted shell. Bootstrap не заменяет firewall/SSH configuration и не устанавливает автоматические резервные копии. Если Docker уже есть, отдельно проверьте остальные prerequisites.

Host key

Получите host key и сравните fingerprint с доверенным источником, например консолью провайдера или ранее подтверждённым known_hosts. ssh-keyscan сам по себе не подтверждает подлинность сервера. В workflow используется StrictHostKeyChecking=yes; не заменяйте его отключением проверки при ошибке подключения.

Secrets и variables GitHub

ТипИмяЗначение
SecretVPS_SSH_KEYПриватный dedicated deployment key
SecretVPS_KNOWN_HOSTSПодтверждённые known_hosts entries для VPS
VariableVPS_HOSTgheilt.mxsource.xyz
VariableVPS_READYtrue после завершения подготовки

Пример передачи ключа без вставки в командную строку:

gh secret set VPS_SSH_KEY --repo maderwin/gheilt < .local/deploy_ed25519
gh secret set VPS_KNOWN_HOSTS --repo maderwin/gheilt < .local/known_hosts
gh variable set VPS_HOST --repo maderwin/gheilt --body gheilt.mxsource.xyz
gh variable set VPS_READY --repo maderwin/gheilt --body true

.local/known_hosts здесь означает заранее подготовленный файл, а не файл, который создаёт setup проекта. Пароли PostgreSQL и ключи администратора CMS создаются на сервере. Read-only Content API token отдельно передаётся в GitHub secret для статической сборки web; это не пароль администратора.

Pipeline

Исходник схемы
flowchart LR
  Commit[Push main] --> Check[Изолированная CMS + check + ISR smoke]
  Check --> Images[Сборка web/functions/Strapi/панели задач]
  Images --> GHCR[GHCR: SHA tags и digest]
  GHCR --> SSH[SSH + release files]
  SSH --> Infra[PG + Hatchet + seed/token]
  Infra --> Apps[up --wait приложений]
  Apps --> Health[HTTPS health]
  Health --> Current[current на новый release]

Pull request в main выполняет только check. Push в main выполняет check, images и deploy; deploy требует VPS_READY=true. Manual workflow_dispatch также поддержан, но сборка образов ограничена ref main.

Actions закреплены commit SHA. Jobs имеют timeout. Concurrency group связан с ref; у активного запуска cancel-in-progress: false, но это не обещание сохранения всех pending-запусков в очереди GitHub. На самом VPS отдельную блокировку даёт flock.

Для GHCR используется временный GITHUB_TOKEN: packages: write при публикации, packages: read при деплое. Registry login передаёт token через stdin по SSH. Временный Docker config /opt/gheilt/.registry-<run-id> удаляется trap при завершении; долгоживущий registry PAT в shared .env не требуется. Потеря runner/связи может помешать cleanup — контролируйте оставшиеся временные каталоги.

Что происходит на сервере

Release script удерживает deploy flock, дополняет .env без ротации старых ключей, проверяет Compose и скачивает закреплённые images. PostgreSQL/Hatchet/Garage должны стать healthy; Garage website разрешается явно. Worker token provisioning сохранён.

Для первой миграции перед новой CMS выполняется cold backup: остановка писателей и Garage, pg_dump Strapi, tar metadata/data/local uploads, environment/release refs. Проверяется restore базы в scratch DB и Garage в отдельных volumes/network. Исходные volumes не удаляются; backup остаётся на том же VPS, внешнюю копию нужно делать отдельно.

Затем новая Strapi стартует, read-only Content API token создаётся или восстанавливается, его hash проверяется и значение сохраняется без печати. Compose пересоздаёт CMS, если добавился token env. CONTENT_READY=false оставляет старый web и записывает symlink /opt/gheilt/prepared; это успешная подготовка CMS, а не переключение сайта.

Snapshot bind mount должен читаться uid 1000 пользователя node в CMS image. Для защищённого host-каталога используйте файл 0600 с владельцем uid1000; файл 0600 root контейнер прочитать не сможет. Снимок содержит публичный контент, токены в него не входят.

На подготовленном релизе выполните явный импорт с одним процессом CMS: остановите CMS, запустите одноразовый importer с snapshot, сохраните report, верните CMS. Failed документы блокируют следующий шаг. Сверьте records и created/updated/skipped/failed, проверьте published content и media. Только затем установите CONTENT_READY=true и запустите новый main workflow_dispatch. Перед первым переключением проверяются Site/logo и исходные collections. Обычный релиз применяет web и проверяет process health плюс /api/content-health, валидирующий весь опубликованный контент. Пустая коллекция после редакторского unpublish допустима.

Importer никогда не запускается из bootstrap. Обычный deploy не повторяет source import и не перезаписывает редакторские правки. Content readiness дополняет process health; он не заменяет ручной editor workflow или подтверждение работоспособности worker.

Откат: что гарантируется

Если применение web или HTTP smoke провалилось, script возвращает прежние web, functions и Caddy с --no-deps. CMS, PostgreSQL и Garage сохраняют новое состояние: автоматически запускать старый CMS image против новой базы небезопасно.

Image rollback не восстанавливает SQL/media и не отменяет уже выполненные задачи. Для отката CMS/data нужны согласованная копия, окно обслуживания и отдельная проверка schemas/provider. Одноузловой VPS, отсутствие blue-green и cold backup дают короткие перерывы доступности. Старый JSON web — только переходный release, не рабочий fallback внутри нового приложения.

Проверка после релиза

gh run list --repo maderwin/gheilt --workflow deploy.yaml --limit 5
curl --fail https://gheilt.mxsource.xyz/api/health
curl --fail https://gheilt.mxsource.xyz/robots.txt
curl -I https://cms.gheilt.mxsource.xyz/admin
curl -I https://hatchet.gheilt.mxsource.xyz

Страницы входа CMS/Hatchet доступны без HTTP Basic Auth. До открытия новой CMS наружу создайте администратора командой npm run strapi -- admin:create-user в контейнере Strapi. В текущей тестовой инсталляции администратор уже создан; данные входа сохранены локально в .local/production-access.txt. Проверьте, что защищённые API без учётной записи отклоняют доступ, а вход администратора и запросы API после входа работают без окна пароля браузера. Отдельно проверьте worker connection и исполнение тестового сообщения. В упражнении используйте обозначенный тестовый payload без персональных данных.

Для другого VPS одного DNS недостаточно: проверьте архитектуру CPU, URL в setup/override/workflow/release, cookie domain, known_hosts и доступ к GHCR.

Где смотреть код

Workflow, bootstrap, release, seed/token, production setup.