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

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

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

Имена проекта и миграция существующего VPS

Workspace-пакеты используют scope @atmanki, Compose-проект — atmanki, пользователь и служебная база PostgreSQL — atmanki, SSH-пользователь — atmanki-deploy. Домены, репозиторий maderwin/gheilt и /opt/gheilt сохранены.

Перенос существующей установки выполнен отдельно от деплоя образов: резервная копия, проверки и границы отката. На другом старом стенде нельзя просто запустить новый compose up: новое имя проекта создаёт другие тома, а POSTGRES_USER и POSTGRES_DB действуют только при первичной инициализации. Сначала нужны согласованная копия, перенос томов, переименование служебной роли и базы, общая сеть и проверка SSH-пользователя.

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

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

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

На 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/Strapi]
  Images --> GHCR[GHCR: SHA tags и digest]
  GHCR --> SSH[SSH + release files]
  SSH --> Infra[PG + Garage]
  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 lock, сохраняет текущие секреты и скачивает готовые образы. Запускает PostgreSQL, Garage и CMS, затем проверяет сайт. После успешного обновления удаляются контейнеры прежнего планировщика и воркера. Их старые данные автоматически не удаляются.

Для первой миграции перед новой 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.

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

Если применение web или HTTP smoke провалилось, script возвращает прежние web, 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

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

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

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

Workflow, release, подготовка окружения.

Выпуск наблюдаемости

.github/workflows/observability.yaml проверяет конфигурацию и реальные миграции Umami/GlitchTip с временной PostgreSQL, затем при ручном запуске с deploy=true может применить точный проверенный ref. Он не собирает приложения на VPS. Образы сторонних сервисов закреплены digest в infra/observability/images.env.

gh workflow run observability.yaml --ref REPLACE_WITH_CHECKED_REF -f deploy=true

Workflow должен присутствовать в default branch, а ref — содержать проверенные файлы релиза. Скрипт использует общий /opt/gheilt/deploy.lock, отдельные /opt/gheilt/observability/releases и current, запускает только свой project. Маршруты трёх панелей добавляются к работающему Caddy, обычный app deploy сохраняет активный fragment observability. Docs/Storybook не удаляют существующие маршруты.

Порядок первого подключения: приёмники и аккаунты → website/project IDs → CI variables и source-map secret → новый web-образ → проверка событий. Нельзя объявить интеграцию готовой после старта пустой панели. Откат образов не откатывает миграции баз аналитики; заранее нужны резервные копии.

Переход на SourceCraft

SourceCraft уже собирает четыре application images в YC Registry; VPS получает готовые digest каждые две минуты. Production пока работает через описанный выше GitHub pipeline. Интеграция main→test и PR lifecycle выполняется отдельно: сборка и получение, полные стенды.