CyberTrainデプロイ

本番環境にデプロイする

CyberTrain のアプリは、1 本のネイティブバイナリとその横のフォルダとして配ります。バイナリが話すのは素の HTTP なので、前段の何かが HTTPS を受け持ちます:Kamal でデプロイするなら kamal-proxy、自分で管理するサーバーなら Caddy です。このページでは、各部品がどう組み合わさるかと、2 つのやり方の違いを説明します。コマンドの一つひとつは、各ステップからリンクしているガイドにあります。

必要なもの
SSH で入れる Linux サーバーと、ドメイン
動かすもの
リバースプロキシの後ろにバイナリ 1 本、その横に SQLite
2 つの方法
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 にリンクします。そのため、動かすマシンと同じ OS・CPU でビルドします。Kamal の構成のように Docker イメージの中でビルドすれば、これは自動的に満たされます。
  • 話すのは素の HTTP/1.1 だけです。TLS と HTTP/2 はリバースプロキシの役目です。本番ではセッション cookie が Secure なので、HTTPS で配信しないとフォームが Invalid authenticity token で失敗します。
  • データベースは SQLite のファイル 1 つです。1 台のサーバーで 1 プロセスとして動かし、ファイルはデプロイをまたいで残る場所に置きます。
  • 既定で本番モードです。CYBERTRAIN_SECRET_KEY_BASE がないと起動しません。./blog migrate はバイナリに組み込まれたマイグレーションを適用します。

2 つのデプロイ方法

なぜどちらも同じバイナリの前に HTTPS を置きます。違うのは、どこでビルドするかと、デプロイでどう入れ替えるかです。

Kamalsystemd と Caddy
ビルドする場所Docker イメージの中。手元のマシンかリモートのビルダーでサーバー上で、git のチェックアウトから
前段kamal-proxy。Let’s Encrypt の証明書付きCaddy。Let’s Encrypt の証明書付き
静的ファイルアプリが public/ を返すCaddy が dist/public/ を直接返す
デプロイ新しいコンテナはヘルスチェックに通ってからトラフィックを受ける:接続は拒否されない再起動:数秒間 Caddy の 500.html が出る
ロールバックkamal rollback VERSIONcurrent を前のリリースに向けて再起動
サーバーに必要なものSSH と Docker(kamal setup が入れる)、それにイメージのレジストリRuby、C ツールチェーン、SQLite のヘッダ、cybertrain CLI、Spinel

どちらでも、アプリは 1 台のサーバー上の 1 プロセスと、その SQLite ファイルです。

Kamal でデプロイする

なぜファイルは cybertrain new がもう書いています。埋めるのはサーバー、ドメイン、レジストリだけです。

  • DockerfileUbuntu のビルドステージで cybertrain setup と cybertrain build を実行し、同じリリースの軽量なステージに dist/ とリンクするライブラリだけを入れる
  • config/deploy.ymlサーバー、ドメイン、ポート 3000 への TLS 付き kamal-proxy、ヘルスチェック、データベースのボリューム
  • .kamal/secretsKAMAL_REGISTRY_PASSWORD と CYBERTRAIN_SECRET_KEY_BASE を環境変数から読む。値そのものは書かない
  • public/up.html新しいコンテナにトラフィックを送る前に kamal-proxy が確かめるページ

config/deploy.yml の CHANGE ME の値を埋めてコミットし、次を実行します:

ターミナル — blog/shell
export KAMAL_REGISTRY_PASSWORD=...
export CYBERTRAIN_SECRET_KEY_BASE="$(openssl rand -hex 32)"  # 一度だけ作り、保管する
kamal setup    # 初回:サーバーに Docker と kamal-proxy、ビルド、push、起動
kamal deploy   # 以降のデプロイ
  • デプロイのたびにイメージをビルドして(Spinel は初回のあと Docker のレイヤーキャッシュに残ります)push し、コンテナを起動します。コンテナは ./blog migrate を実行してから、exec ./blog で 0.0.0.0:3000 を待ち受けます。
  • kamal-proxy は /up.html が 200 を返すまで待ってからトラフィックを新しいコンテナに切り替え、古いコンテナを止めます。古いコンテナは SIGTERM を受けて、処理中のリクエストを終えてから終了します。
  • データベースは /app/storage にマウントされたボリューム blog_storage にあるので、デプロイしても残ります。
  • builder.arch はサーバーの CPU に合わせます。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.md にあります(英語版は docs/deploy.en.md)。

設定する

なぜ本番の設定は環境変数から来るので、同じバイナリがどこでも動きます。

変数本番では
CYBERTRAIN_SECRET_KEY_BASE必須。セッション cookie に署名します。変えると全員がログアウトされます。openssl rand -hex 32 で一度だけ作ります
CYBERTRAIN_DATABASESQLite のファイル。既定は作業ディレクトリの storage/production.sqlite3。リリースの外に置きます
CYBERTRAIN_HOST既定は 127.0.0.1 で、同じマシンのプロキシの後ろならこれで正しい。コンテナでは 0.0.0.0
PORT既定は 3000。プロキシの転送先
CYBERTRAIN_ENVビルドしたバイナリではすでに production。development にするとファイル監視と再ビルドが始まる

チュートリアルのデプロイ用にビルドするステップに、すべての変数と対応する config/app.rb の属性があります。

日々の運用

  • 変更はすべてデプロイです。ビューはビルド時に埋め込まれるので、ビューだけの変更でも再ビルドと再起動が要ります。gen/ も一緒にコミットしてください:デプロイスクリプトは古い gen/ で止まります。
  • マイグレーションは新しいコードが動き出す前に流れます:デプロイスクリプトの中か、コンテナの起動時です。列の削除は、動いているコードがその列を読まなくなってから、別のデプロイで行います。
  • バックアップは SQLite の .backup で取ります。ファイルのコピーではなく:アプリが書き込んでいる間でも一貫したコピーが取れます。
  • マイグレーションをまたいでロールバックするときは、切り替える前に、新しい方のリリースから db rollback でデータベースも戻します。
  • 停止は丁寧です:SIGTERM を受けると新しい受け付けを止め、処理中のリクエストを終えて終了します。長くても 10 秒です。

次に読むもの