Puma を 7 から 8 にメジャーアップグレードするときの注意点

2026/6/5

代表

Puma を 7.x から 8.x に上げるとき、変わるのは依存ロックファイルの 1 行だけに見えても、メジャーバージョンには本番でしか顕在化しない破壊的変更が含まれています。この記事では、Puma 8 へ上げる前に最低限おさえておきたい注意点を、bind アドレスの変更プラグイン互換性の 2 点に絞って整理します。

注: バージョンごとの挙動は公式の Version 8 Upgrade GuideHistory.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.rbbind host を明示していない設定だと、アップグレードした瞬間にデフォルトの待ち受けアドレスが変わります。

# host を書いていない = デフォルトに従う(← 7 と 8 で挙動が変わる)
port ENV.fetch("PORT", 3000)

何が起きているか(gem 内部のロジック)

挙動は gem のソースで確認できます(puma-8.0.2/lib/puma/configuration.rb)。

  • default_tcp_hostipv6_interface_available? ? "[::]" : "0.0.0.0"
  • ipv6_interface_available?Socket.ip_address_list非ループバックの IPv6 アドレスaddr.ipv6? && !addr.ipv6_loopback?)が 1 つでもあれば true
  • rewrite_unavailable_ipv6_binds! … IPv6 が無い環境では :: 指定を 0.0.0.0 に書き戻す

要するに「コンテナ/ホストに実 IPv6 アドレスがあるかどうか」で待ち受け先が ::0.0.0.0 に分岐します。

具体的にどういうケースで壊れるのか

配信経路がリバースプロキシ(Nginx / kamal-proxy / Thruster など)→ Puma という構成のとき、影響するのはプロキシ → Puma のループバック 1 区間だけです。そして実際に壊れるのは、次の 3 条件がすべて揃ったときに限られます。

  1. コンテナ/ホストに非ループバックの IPv6 インターフェースがある → Puma 8 が [::]:PORT に bind する
  2. カーネルが net.ipv6.bindv6only=1[::] ソケットが IPv4(v4-mapped)接続を受け付けない
  3. プロキシが 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 したかを見れば一発です。

# ① Puma の起動ログ。"0.0.0.0" か "[::]" かがそのまま出る
#   * Listening on http://0.0.0.0:3000   ← IPv4。7 と同じ
#   * Listening on http://[::]:3000       ← IPv6。条件 1 が成立

# ② コンテナ内に非ループバック IPv6 があるか(Puma 8 の判定そのもの)
ruby -rsocket -e 'p Socket.ip_address_list.select { |a| a.ipv6? && !a.ipv6_loopback? }.map(&:ip_address)'
#   [] なら 0.0.0.0 にフォールバック

# ③ bindv6only の既定(条件 2)
cat /proc/sys/net/ipv6/bindv6only   # 0 = IPv4 も受理 / 1 = IPv6 のみ

実際、IPv6 を無効化した一般的な Docker コンテナでは、Puma 8 でも起動ログは Listening on http://0.0.0.0:PORT、非ループバック IPv6 は []bindv6only0——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 を明示してアップグレード前の挙動に固定しておくのが確実です。

# config/puma.rb
port ENV.fetch("PORT", 3000), "0.0.0.0"
# または
bind "tcp://0.0.0.0:#{ENV.fetch('PORT', 3000)}"

1 行・副作用ゼロの保険です。入れるなら Puma 8 の破壊的変更への追従なので、Puma を上げる変更と同じコミット/PR にまとめておくのが筋です(同じ関心事をまとめ、リバートも容易にする)。


2. Puma プラグインを使っているなら起動確認を忘れずに

Puma 8 は内部の大きなリファクタを含みます。config/puma.rbplugin :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 を使っているなら、本番に近い環境でプラグインの起動確認をする

© 2026 つくばAIラボ株式会社