CyberTrainРазвёртывание

Развёртывание в продакшене

Приложение CyberTrain поставляется как один нативный двоичный файл и каталог рядом с ним. Оно говорит на обычном HTTP, поэтому HTTPS берёт на себя то, что стоит перед ним: kamal-proxy, если вы развёртываете через Kamal, или Caddy на сервере, которым вы управляете сами. Эта страница объясняет, как сочетаются части и чем отличаются два способа; все команды есть в руководствах, на которые ссылается каждый шаг.

Нужно
Linux-сервер с доступом по SSH и домен
Работает
Один двоичный файл за обратным прокси, рядом SQLite
Два способа
Kamal и Docker или systemd и Caddy

Вы здесь:Время выполнения dist/

Что работает в продакшене

ЗачемЗная, что делает двоичный файл и что он оставляет другим, вы определяете всё остальное в настройке.

cybertrain build компилирует фреймворк, ваши контроллеры и модели и интерпретатор шаблонов в dist/blog, встраивая все представления, и копирует рядом public/. Там, где он работает, больше ничего не устанавливается: ни Ruby, ни gem.

продакшенtext
browser ──HTTPS──▶ reverse proxy (kamal-proxy or Caddy)
                     └─ HTTP/1.1 ──▶ dist/blog (port 3000)
                                       ├─ public/  static files, 404.html, 500.html
                                       └─ storage/production.sqlite3
  • Он связан с системными SQLite и libc, поэтому собирайте его на той же ОС и том же процессоре, что и машина, на которой он работает. Сборка внутри образа Docker, как в конфигурации Kamal, решает это сама.
  • Он говорит только на обычном HTTP/1.1. TLS и HTTP/2 — задача обратного прокси. В продакшене cookie сессии имеет атрибут Secure, поэтому сайт должен отдаваться по HTTPS, иначе формы падают с Invalid authenticity token.
  • База данных — один файл SQLite. Запускайте один процесс на одном сервере и храните файл там, где он переживает развёртывание.
  • По умолчанию он в режиме продакшена и не запускается без CYBERTRAIN_SECRET_KEY_BASE. ./blog migrate применяет встроенные в него миграции.

Два способа развёртывания

ЗачемОба ставят HTTPS перед одним и тем же двоичным файлом; различаются они тем, где он собирается и как развёртывание его заменяет.

Kamalsystemd и Caddy
Где собираетсяВ образе Docker, на вашей машине или удалённом сборщикеНа сервере, из рабочей копии git
Перед приложениемkamal-proxy с сертификатом Let’s EncryptCaddy с сертификатом Let’s Encrypt
Статические файлыПриложение отдаёт public/Caddy сам отдаёт dist/public/
РазвёртываниеНовый контейнер получает трафик после проверки работоспособности: ни одно соединение не отклоняетсяПерезапуск: несколько секунд Caddy отдаёт 500.html
Откатkamal rollback VERSIONНаправить current на предыдущий выпуск и перезапустить
Серверу нужноSSH и Docker (его ставит kamal setup), а также реестр образовRuby, набор инструментов C, заголовки SQLite, CLI cybertrain и Spinel

В любом случае приложение — это один процесс и его файл SQLite на одном сервере.

Развёртывание через Kamal

Зачемcybertrain new уже написал файлы; вам остаётся указать сервер, домен и реестр.

  • Dockerfileэтап сборки на Ubuntu запускает cybertrain setup и cybertrain build; облегчённый этап того же выпуска получает только dist/ и библиотеки, с которыми он связан
  • config/deploy.ymlсервер, домен, kamal-proxy с TLS на порт 3000, проверка работоспособности и том базы данных
  • .kamal/secretsчитает KAMAL_REGISTRY_PASSWORD и CYBERTRAIN_SECRET_KEY_BASE из вашего окружения, но никогда не хранит сами значения
  • public/up.htmlстраница, которую kamal-proxy опрашивает, прежде чем направить трафик в новый контейнер

Заполните значения, помеченные CHANGE ME, в config/deploy.yml, сделайте коммит и выполните:

терминал — blog/shell
export KAMAL_REGISTRY_PASSWORD=...
export CYBERTRAIN_SECRET_KEY_BASE="$(openssl rand -hex 32)"  # один раз; сохраните его
kamal setup    # первый раз: Docker и kamal-proxy на сервере, сборка, публикация, запуск
kamal deploy   # каждое следующее развёртывание
  • Каждое развёртывание собирает образ (после первого раза Spinel остаётся в кеше слоёв Docker), публикует его и запускает контейнер, который выполняет ./blog migrate, а затем exec ./blog на 0.0.0.0:3000.
  • kamal-proxy ждёт, пока /up.html не ответит 200, переключает трафик на новый контейнер и останавливает старый, который по SIGTERM завершает обрабатываемые запросы.
  • База данных лежит на томе blog_storage, смонтированном в /app/storage, поэтому переживает каждое развёртывание.
  • Укажите в builder.arch процессор сервера. Spinel компилируется внутри образа, поэтому сборка для другой архитектуры под эмуляцией медленная; вместо неё собирайте на сервере через builder.remote.

Пошаговое руководство, включая резервное копирование тома, — docs/deploy-kamal.md. В examples/blog та же конфигурация, и CI собирает и проверяет его образ.

За Caddy, под systemd

ЗачемБез Docker: сервер собирает приложение из git, systemd поддерживает его работу, Caddy смотрит в интернет.

  • Приложение слушает только 127.0.0.1:3000, поэтому до него не добраться иначе как через Caddy. public/ и storage/ разрешаются от рабочего каталога, поэтому systemd запускает его с рабочим каталогом dist/ и читает секрет из файла окружения вне репозитория.
  • Скрипт развёртывания клонирует каждый выпуск в отдельный каталог, проверяет, что gen/ актуален, выполняет cybertrain build и dist/blog migrate, затем направляет на него ссылку current, перезапускает службу и ждёт ответа up.html; если ответа нет, скрипт возвращает предыдущий выпуск. Ошибка до переключения работающий выпуск не затрагивает. Возврат не отменяет миграции, поэтому они должны оставаться совместимыми с предыдущим выпуском; при неудачном первом развёртывании возвращаться некуда, и скрипт просто завершается с ошибкой.
  • Caddy получает сертификат, перенаправляет HTTP на HTTPS, сам отдаёт файлы из dist/public/ и проксирует всё остальное в приложение. Пока приложение перезапускается, он отвечает 500.html.
/etc/caddy/Caddyfilecaddyfile
blog.example.com {
	encode zstd gzip
	root * /srv/blog/current/dist/public

	@static file
	handle @static {
		file_server
	}

	handle {
		reverse_proxy 127.0.0.1:3000
	}

	handle_errors {
		rewrite * /500.html
		file_server
	}
}

Приложение добавляет X-Content-Type-Options и X-Frame-Options к тому, что отдаёт; файлы, которые Caddy отдаёт из dist/public/, через приложение не проходят, так что, если они нужны и там, добавьте их в блок сайта. С nginx всё так же: завершить TLS, отдавать dist/public/, проксировать остальное.

Полное руководство, от нового сервера Ubuntu до резервных копий и откатов, — docs/deploy.en.md (японский оригинал — docs/deploy.md).

Настройка

ЗачемНастройки продакшена берутся из окружения, поэтому один и тот же двоичный файл работает где угодно.

ПеременнаяВ продакшене
CYBERTRAIN_SECRET_KEY_BASEОбязательна. Подписывает cookie сессии; её смена разлогинивает всех. Создайте один раз командой openssl rand -hex 32
CYBERTRAIN_DATABASEФайл SQLite; по умолчанию storage/production.sqlite3 в рабочем каталоге. Держите его вне выпуска
CYBERTRAIN_HOSTПо умолчанию 127.0.0.1, что верно за прокси на той же машине; в контейнере 0.0.0.0
PORTПо умолчанию 3000; туда пересылает прокси
CYBERTRAIN_ENVВ собранном двоичном файле уже production; development запустил бы слежение за файлами и пересборки

Шаг учебника Сборка для развёртывания перечисляет все переменные и соответствующие им атрибуты config/app.rb.

Повседневная работа

  • Любое изменение — это развёртывание. Представления встраиваются при сборке, поэтому даже изменение только представления требует пересборки и перезапуска. Коммитьте gen/ вместе с изменением: скрипт развёртывания останавливается на устаревшем gen/.
  • Миграции выполняются до того, как новый код начнёт отвечать: в скрипте развёртывания или при запуске контейнера. Удаляйте столбец в одном из следующих развёртываний, когда работающий код его уже не читает.
  • Делайте резервные копии через .backup SQLite, а не копированием файла: так получается согласованная копия, даже пока приложение пишет.
  • Откат через миграцию означает откат и базы данных: перед переключением выполните db rollback из более нового выпуска.
  • Остановка аккуратная: по SIGTERM сервер перестаёт принимать соединения, завершает обрабатываемые запросы и выходит, не позже чем через 10 секунд.

Что дальше