GitHub Actions и развёртывание на VPS
Оглавление · Далее: эксплуатация
Текущая схема
GitHub repository — maderwin/gheilt, deployment branch — main.
Сервисы находятся на VPS gheilt.mxsource.xyz; его поддомены должны указывать туда же.
Образы CI собирает для linux/amd64. Перенос на ARM-хост требует изменения platforms.
| Домен | Сервис |
|---|---|
gheilt.mxsource.xyz | Next.js |
cms.gheilt.mxsource.xyz | Strapi с собственным входом |
hatchet.gheilt.mxsource.xyz | Hatchet 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
| Тип | Имя | Значение |
|---|---|---|
| Secret | VPS_SSH_KEY | Приватный dedicated deployment key |
| Secret | VPS_KNOWN_HOSTS | Подтверждённые known_hosts entries для VPS |
| Variable | VPS_HOST | gheilt.mxsource.xyz |
| Variable | VPS_READY | true после завершения подготовки |
Пример передачи ключа без вставки в командную строку:
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.