Puma を 7 から 8 にメジャーアップグレードするときの注意点
2026/6/5
代表
Puma を 7.x から 8.x に上げるとき、変わるのは依存ロックファイルの 1 行だけに見えても、メジャーバージョンには本番でしか顕在化しない破壊的変更が含まれています。この記事では、Puma 8 へ上げる前に最低限おさえておきたい注意点を、bind アドレスの変更とプラグイン互換性の 2 点に絞って整理します。
注: バージョンごとの挙動は公式の Version 8 Upgrade Guide と
History.mdが一次情報です。本記事は Puma 8.0 系の変更点に基づきます。なお 8.0.2 では PROXY protocol v1 パーサの脆弱性 2 件(CVE-2026-47736 / CVE-2026-47737)も修正されており、セキュリティ面でも上げる価値があります。
1. 最大の落とし穴 — 本番デフォルトの bind が IPv6 になった
Puma 8.0.0 の breaking change のうち、最も影響が大きいのがこれです。
本番環境のデフォルト bind アドレスが
0.0.0.0から::(IPv6)に変わった。 非ループバックの IPv6 インターフェースが存在する場合に::を使い、IPv6 が利用できなければ0.0.0.0にフォールバックする。
つまり config/puma.rb で bind host を明示していない設定だと、アップグレードした瞬間にデフォルトの待ち受けアドレスが変わります。
何が起きているか(gem 内部のロジック)
挙動は gem のソースで確認できます(puma-8.0.2/lib/puma/configuration.rb)。
default_tcp_host…ipv6_interface_available? ? "[::]" : "0.0.0.0"ipv6_interface_available?…Socket.ip_address_listに非ループバックの IPv6 アドレス(addr.ipv6? && !addr.ipv6_loopback?)が 1 つでもあればtruerewrite_unavailable_ipv6_binds!… IPv6 が無い環境では::指定を0.0.0.0に書き戻す
要するに「コンテナ/ホストに実 IPv6 アドレスがあるかどうか」で待ち受け先が :: か 0.0.0.0 に分岐します。
具体的にどういうケースで壊れるのか
配信経路がリバースプロキシ(Nginx / kamal-proxy / Thruster など)→ Puma という構成のとき、影響するのはプロキシ → Puma のループバック 1 区間だけです。そして実際に壊れるのは、次の 3 条件がすべて揃ったときに限られます。
- コンテナ/ホストに非ループバックの IPv6 インターフェースがある → Puma 8 が
[::]:PORTに bind する - カーネルが
net.ipv6.bindv6only=1→[::]ソケットが IPv4(v4-mapped)接続を受け付けない - プロキシが Puma へ IPv4 ループバック(
127.0.0.1)で接続し、::1にフォールバックしない
この 3 つが同時成立すると、プロキシ → Puma が connection refused になり、本番でだけ「502 / connection refused」 という形で踏みます。逆に言えば、どれか 1 つでも崩れれば正常に動きます。
- Linux の
net.ipv6.bindv6only既定は0で、[::]ソケットでも IPv4-mapped 経由で IPv4 接続を受理する(→ 条件 2 が不成立)。 - Docker は既定で IPv6 を無効にするため、コンテナ内に非ループバック IPv6 が無く
0.0.0.0にフォールバックする(→ 条件 1 が不成立)ことが多い。
つまり多くの環境ではそもそも壊れる条件が立ちません。だからこそ「本番でだけ、しかも特定構成でだけ」顕在化する、見つけにくいエッジケースになります。
自分の環境がどちらに転ぶか確認する
推測せず、上げたあと(または検証環境)で実際に何に bind したかを見れば一発です。
実際、IPv6 を無効化した一般的な Docker コンテナでは、Puma 8 でも起動ログは Listening on http://0.0.0.0:PORT、非ループバック IPv6 は []、bindv6only は 0——Puma 7 と完全に同一挙動になります。
補足: net.ipv6.bindv6only=1 はいつ使われるのか
条件 2 の bindv6only=1 は Linux の既定ではありません(既定は 0)。意図的に選んだときだけ現れる少数派の設定で、主な用途は次のとおりです。
- セキュリティ・ハードニング:
[::]が IPv4 を受けると相手は::ffff:a.b.c.dという IPv4-mapped アドレスで見えます。これが IP ベースの ACL すり抜けやログ/レート制限の誤判定の温床になるため、v4 と v6 を別ソケットで明示分離する目的で1にする(CIS 系の厳格構成など)。 - アプリが v4 / v6 を自前で別々に bind する: 同一ポートに
0.0.0.0と[::]を両立させるには v6only が必要(nginx のlisten ... ipv6only=on既定、Java の歴史的挙動など)。その既定値として1を敷くことがある。 - BSD 系 / RFC 3493 セマンティクスの踏襲: 「v4 と v6 のソケットは独立」を厳密に守る、あるいは他 OS と挙動を揃えたい場合。
つまり条件 2 は「ハードニングや特殊なネットワーク方針を明示的に入れた環境」でしか立ちません。
対策: 必要なら bind host を明示して挙動を固定する
確認の結果すでに 0.0.0.0 にフォールバックしているなら、bind 変更は実質 no-opで、今すぐの対応は不要です。一方で「将来 Docker ネットワークで IPv6 を有効化した」「グローバル IPv6 を持つ base image / ホストに移した」「bindv6only=1 の環境に載せた」といった構成変更で静かに 502 化するのを避けたいなら、host を明示してアップグレード前の挙動に固定しておくのが確実です。
1 行・副作用ゼロの保険です。入れるなら Puma 8 の破壊的変更への追従なので、Puma を上げる変更と同じコミット/PR にまとめておくのが筋です(同じ関心事をまとめ、リバートも容易にする)。
2. Puma プラグインを使っているなら起動確認を忘れずに
Puma 8 は内部の大きなリファクタを含みます。config/puma.rb に plugin :xxx の行があるなら、上げたあとに実際にプロセスを起動して、そのプラグインが正常に動くか確認してください。プラグインの不整合は単体テストでは捕捉できず、起動して初めて分かるたちのものです。CI のグリーンを鵜呑みにせず、本番に近い環境での起動確認まで済ませてからマージしましょう。
まとめ — アップグレード前のチェックリスト
-
config/puma.rbで bind host を明示しているか確認する - 上げたあとの Puma 起動ログ(
Listening on ...)が0.0.0.0か[::]かを実機で確認する - リバースプロキシ → Puma のループバック接続が IPv4(
127.0.0.1)依存になっていないか確認する - 「非ループバック IPv6 あり +
bindv6only=1+ プロキシが IPv4 接続」の 3 条件に該当しそうなら、0.0.0.0を明示して挙動を固定する(該当しなければ実質 no-op だが、将来の構成変更に対する保険として入れてもよい) -
plugin :xxxを使っているなら、本番に近い環境でプラグインの起動確認をする