現在地:実行時 dist/
本番で動くもの
なぜバイナリが何をして、何を他に任せるのかが分かれば、残りの構成は自然に決まります。
cybertrain build は、フレームワーク、あなたのコントローラとモデル、テンプレートインタプリタを dist/blog にコンパイルし、すべてのビューを埋め込み、その横に public/ をコピーします。動かす場所には他に何も入れません:Ruby も gem も不要です。
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 を置きます。違うのは、どこでビルドするかと、デプロイでどう入れ替えるかです。
| Kamal | systemd と Caddy | |
|---|---|---|
| ビルドする場所 | Docker イメージの中。手元のマシンかリモートのビルダーで | サーバー上で、git のチェックアウトから |
| 前段 | kamal-proxy。Let’s Encrypt の証明書付き | Caddy。Let’s Encrypt の証明書付き |
| 静的ファイル | アプリが public/ を返す | Caddy が dist/public/ を直接返す |
| デプロイ | 新しいコンテナはヘルスチェックに通ってからトラフィックを受ける:接続は拒否されない | 再起動:数秒間 Caddy の 500.html が出る |
| ロールバック | kamal rollback VERSION | current を前のリリースに向けて再起動 |
| サーバーに必要なもの | 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 の値を埋めてコミットし、次を実行します:
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を返します。
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_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/で止まります。 - マイグレーションは新しいコードが動き出す前に流れます:デプロイスクリプトの中か、コンテナの起動時です。列の削除は、動いているコードがその列を読まなくなってから、別のデプロイで行います。
- バックアップは SQLite の
.backupで取ります。ファイルのコピーではなく:アプリが書き込んでいる間でも一貫したコピーが取れます。 - マイグレーションをまたいでロールバックするときは、切り替える前に、新しい方のリリースから
db rollbackでデータベースも戻します。 - 停止は丁寧です:
SIGTERMを受けると新しい受け付けを止め、処理中のリクエストを終えて終了します。長くても 10 秒です。