kamalを試してみた(そして諦めた…)

2026-06-17 rails linux docker

背景

長年Apache + Passengerで運用してきたRailsアプリ群を新環境へ移行することにしました。ちょうど良い機会なので、Passengerからの脱却も同時に検証したいと考え、Kamal 2を試すことにしました。検証環境にはオフィスのRaspberry Piを使いました。クラウドで使い捨てのVMを立てる案もありましたが、無料で何度でも試せる点を優先しました。検証対象は2本のRailsアプリ(test1: SQLite、test2: MariaDB)で、別々のドメインに割り当てる構成です。

Kamal利用時に想定していた構成

graph TB
    subgraph Mac["MacBook"]
        Build[Docker buildx
イメージビルド] end subgraph Pi["検証サーバー(Raspberry Pi)"] Registry[(ローカルレジストリ
localhost:5555)] Proxy[kamal-proxy
:80 / :443] subgraph Net["Dockerネットワーク kamal"] App1[test1-web
Puma :3000] App2[test2-web
Puma :3000] end SQLite[(SQLite)] MariaDB[(MariaDB)] end Browser[ブラウザ] -->|HTTPS| Router[ルーター
ポートフォワード] Router --> Proxy Proxy -->|host: test1.example.com| App1 Proxy -->|host: test2.example.com| App2 App1 -->|volume| SQLite App2 -->|host.docker.internal| MariaDB Build -->|push| Registry Registry -->|pull| App1 Registry -->|pull| App2

各アプリがDockerコンテナとして独立し、kamal-proxyがドメインベースでルーティングしてTLS終端も担います。ゼロダウンタイムデプロイも標準機能として組み込まれている、というのがKamalの売りでした。

実際にぶつかった問題

1. ビルドにDocker Desktopが必要だった

Kamalはデフォルトで「コマンドを実行している側(今回はMac)」にローカルレジストリ用のコンテナを立てます。MacにDocker Desktopを入れていなかったため、ここで一度止まりました。

ERROR (SSHKit::Command::Failed): docker exit status: 32000
docker stderr: Cannot connect to the Docker daemon at unix:///Users/xxx/.docker/run/docker.sock. 
Is the docker daemon running?

エラーメッセージに/Users/というmacOSのパスが出ているのに気づくまで「検証サーバー側でDockerが必要なのか」と誤解していました。Docker Desktopをインストールして解決しました(個人開発・小規模事業の範囲では無料利用できるようです)。

2. アーキテクチャの指定ミス

deploy.ymlのbuilder.archamd64と誤指定し、Mac(Apple Silicon)でビルドしたイメージを検証サーバー(arm64)にpullしようとして失敗しています。

ERROR (SSHKit::Command::Failed): Exception while executing on host ...: docker exit status: 1
docker stderr: Error response from daemon: no matching manifest for linux/arm64/v8 
in the manifest list entries: no match for platform in manifest: not found

arch: arm64への修正で解消。

3. ヘルスチェックの謎の失敗

ここが一番時間がかかり、かつ未解決の部分です。kamal deployのたびに以下のエラーで止まります。

ERROR Failed to boot web on ...
INFO First web container is unhealthy on ..., not booting any other roles
...
ERROR null
ERROR (SSHKit::Command::Failed): Exception while executing on host ...: docker exit status: 1
docker stderr: Error: target failed to become healthy within configured timeout (30s)

コンテナ自身のログを見ると、Pumaは正常に起動しています。

=> Booting Puma
=> Rails 8.1.3 application starting in production
Puma starting in single mode...
* Puma version: 8.0.2 ("Into the Arena")
* Listening on http://0.0.0.0:3000
Use Ctrl-C to stop

コンテナ内で直接ヘルスチェックを叩くと200が返る。

$ docker exec <container_id> curl -sv http://localhost:3000/up
< HTTP/1.1 200 OK
...
{ [73 bytes data]

アプリは健全なのに、Kamalは「unhealthy」と判定し続ける。当初「Kamal 2のkamal-proxyはヘルスチェックのデフォルトポートが80に変更された」という情報を元にproxy.app_port: 3000を明示したが、症状は変わりませんでした。

実際にKamalが発行しているコマンドをログから確認すると、ポート指定自体は正しかったことが分かります。

INFO Running docker exec kamal-proxy kamal-proxy deploy test1-web 
--target="<container_id>:3000" --host="test1.example.com" --tls 
--deploy-timeout="30s" --drain-timeout="30s" ...

--target<コンテナID>:3000で合っている。つまり「ポート番号の勘違い」という当初の仮説は外れ。

最終的に切り上げた時点では、kamal-proxyコンテナとアプリコンテナが同じDockerネットワーク(kamal)に参加しているにもかかわらず、コンテナ間の到達性そのものに何らかの問題があるのではないかという仮説まで来ていたが、検証用コンテナ(curlimages/curl)でその到達性を実際に確かめる前に時間切れとなり、確証は得られていません。

Kamalを諦めた理由

「今回はここで時間切れにした」というのが実態に近いです。とはいえ、3アプリの移行という当初の目的に対して、Kamalの一つひとつの設定や挙動を都度調べながら進めるコストは、当初の見積もりより高くなりそうでした。筆者がDockerに慣れていないという部分が大きいですが、Dockerコンテナ化そのものが今回の本質的な目的ではなく、あくまでPassengerのプロセス分離問題を解決する手段の一つだったことを考えると、より枯れた方法に切り替える判断は妥当だと考えています。

次にやること:Puma + Nginx構成

graph TB
    subgraph Server["サーバー(本番想定)"]
        WebServer[Nginx
リバースプロキシ + TLS終端
:80 / :443] subgraph App1["app1"] Puma1[Puma :3001] end subgraph App2["app2"] Puma2[Puma :3002] end SQLite[(SQLite)] MariaDB[(MariaDB)] end Browser[ブラウザ] -->|HTTPS| WebServer WebServer -->|test1.example.com| Puma1 WebServer -->|test2.example.com| Puma2 Puma1 --> SQLite Puma2 --> MariaDB

Passengerが内部のRubyインタプリタ共有によって抱えていたgemバージョン競合の問題は、各アプリを独立したPumaプロセスとして起動し、rbenvでアプリごとにRubyバージョンとGemfile.lockを分離することで解消されるはずです。Dockerのようなコンテナ分離を持ち出さずとも、OSプロセスレベルの分離で十分という判断に落ち着いています。

リバースプロキシ層はNginxを採用します。proxy_passでドメインごとにバックエンドのPumaポートへ振り分ける構成はシンプルにかけるはずです。