Restart ポリシーのカスタマイズを可能にしたリバートと実装改善

basecamp/kamal

この PR は、デフォルトで付与されていた --restart unless-stopped をリバートし、アクセサリとロールごとに任意の再起動ポリシーを設定できるようにした変更です。これにより、Init コンテナなど再起動ポリシーが重要になるシナリオでのデプロイが阻害されなくなります。

背景

従来の実装では、docker run コマンドに常に --restart unless-stopped が付与されていました。その結果、Init コンテナで on-failure など特定のポリシーを要求するケースでは、Kamal がエラーを返すか期待通りに動作しませんでした。Issue #1863 では、PR #1503 の変更を元に戻しつつ、再起動ポリシーを柔軟に指定できる仕組みが求められていました。実際に作者は restart: on-failure を設定した Init コンテナで動作確認を取り、問題が解消されたことを報告しています。

技術的な変更

コマンド層の修正

lib/kamal/commands/accessory.rblib/kamal/commands/app.rb で、ハードコードされた unless‑stoppedrestart_policy / role.restart_policy から取得するよう置き換えました。

@@ -4,7 +4,7 @@ class Kamal::Commands::Accessory < Kamal::Commands::Base
   attr_reader :accessory_config
   delegate :service_name, :image, :hosts, :port, :files, :directories, :cmd,
            :network_args, :publish_args, :env_args, :volume_args, :label_args, :option_args,
-           :secrets_io, :secrets_path, :env_directory, :proxy, :running_proxy?, :registry,
+           :restart_policy, :secrets_io, :secrets_path, :env_directory, :proxy, :running_proxy?, :registry,
            to: :accessory_config
@@ -16,7 +16,7 @@ def run(host: nil)
     docker :run,
       "--name", service_name,
       "--detach",
-      "--restart", "unless-stopped",
+      "--restart", restart_policy,
       *network_args,
       *config.logging_args,
@@ -16,7 +16,7 @@ def run(hostname: nil)
     docker :run,
       "--detach",
-      "--restart unless-stopped",
+      "--restart", role.restart_policy,
       "--name", container_name,
       "--network", "kamal",

設定層の拡張

アクセサリ設定

lib/kamal/configuration/accessory.rb に以下の変更を加えました。

  • option_argsrestart キーを除外して Docker オプションを生成するようになりました。
  • restart_policy メソッドが docker_options から restart キーを探し、存在しなければデフォルト unless-stopped を返します。
@@
   def option_args
-    if args = accessory_config["options"]
-      optionize args
-    else
-      []
-    end
+    optionize docker_options.reject { |key, _| key.to_s == "restart" }
   end
+
+  def restart_policy
+    restart_policy_option || "unless-stopped"
+  end
@@
   def docker_options
-    accessory_config["options"] || {}
+    accessory_config["options"] || {}
   end

   def restart_policy_option
-    # previous implementation removed
+    docker_options.find { |key, _| key.to_s == "restart" }&.last
   end

ロール設定

lib/kamal/configuration/role.rb でも同様に option_args から restart を除外し、restart_policy を取得するロジックを追加しました。

@@
   def option_args
-    if args = specializations["options"]
-      optionize args
-    else
-      []
-    end
+    optionize docker_options.reject { |key, _| key.to_s == "restart" }
   end
+
+  def restart_policy
+    restart_policy_option || "unless-stopped"
+  end
@@
   def docker_options
-    specializations["options"] || {}
+    specializations["options"] || {}
   end

   def restart_policy_option
-    # previous implementation removed
+    docker_options.find { |key, _| key.to_s == "restart" }&.last
   end

バリデーションの強化

lib/kamal/configuration/validator.rbvalidate_restart_policy! を追加し、restart オプションが文字列であり、許容される形式(no, always, unless‑stopped, on‑failure, on‑failure:N)に合致するかを検証します。

@@
-      if options
-        error "Cannot set restart policy in docker options, unless-stopped is required" if options["restart"]
+      if restart_policy = options&.find { |key, _| key.to_s == "restart" }
+        validate_restart_policy!(restart_policy.last)
       end
     end
+
+    def validate_restart_policy!(restart_policy)
+      with_context("options/restart") do
+        unless restart_policy.is_a?(String)
+          error %(should be a string. Use "no" to disable restarts)
+        end
+        unless restart_policy.match?(/\A(?:no|always|unless-stopped|on-failure(?::\d+)?)\z/)
+          error "should be no, always, unless-stopped, on-failure, or on-failure:N"
+        end
+      end
+    end

テストの追加

  • アクセサリ・ロールごとの restart_policy 取得テスト
  • docker run コマンドに期待通り --restart が出力されること、execute_in_new_container 系では除外されること
  • 不正な型(boolean)や形式に対するエラーメッセージの検証

設計判断

最小限の破壊的変更で後方互換性を保つ 方針が全体に徹底されています。既存の options ハッシュをそのまま利用し、restart キーだけを特別扱いすることで、ユーザーは従来通り他の Docker オプションを書き続けられ、かつ再起動ポリシーを柔軟に指定できるようになりました。新しい設定キーを導入しなかった点は、設定ファイルの破壊的変更を避ける意図が明確です。

バリデーションを集中管理 したことで、設定ミスが早期に検出され、エラーメッセージは階層的コンテキスト(options/restart)を示すためデバッグが容易になります。これらは Kamal が掲げる「設定は宣言的に、失敗は早期に」の設計哲学と一致します。

まとめ

この PR は、デフォルトの --restart unless-stopped を撤廃し、アクセサリとロールごとに任意の再起動ポリシーを設定できるようにしたリバートと実装改善です。設定構造の拡張は最小限に抑えつつ、バリデーションとテストで堅牢性を高め、Init コンテナなど再起動ポリシーが重要なシナリオで安全にデプロイできるようになりました。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
2b5442ce

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
リトライ後承認
Review Count:
3回 (改善を経て承認)
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文、背景、技術的変更、設計判断(任意)、まとめの3部構成が明確に存在し、まとめはリード文の単なる繰り返しではなく意義を再提示している。

カスタムMarkdown構文 ⚠ WARNING

シンタックスハイライト・GitHubリンク記法の正確性

コードブロックは正しい `言語:ファイルパス` 形式で記載されているが、PRリンクの記法が `[PR #1864](URL)` となっており、要件の `[ #1864 ](URL)` 形式と完全に一致していない。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

内容は Kamal の内部実装に詳しいエンジニア向けで、初心者向けの余計な説明はなく適切な技術レベルになっている。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションは総論→各論→結論の構成で、段落はトピックセンテンスで始まり 1トピックに限定、長さも適切に分割されている。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事中のコードブロックは提供された Diff と完全に一致しており、ファイル名・変更内容ともに正確に反映されている。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

使用されている技術用語(restart policy, docker run, accessory, role など)は PR と Diff に合わせて正確に記述されている。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

技術的な説明は Diff と PR の記述に基づき、因果関係も論理的で誤りがない。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事の全主張は PR のタイトル、説明、Diff、Issue 番号に裏付けられており、追加の推測や外部情報は含まれていない。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR 番号 #1864、Issue #1863、リバート対象 #1503 などの数値・固有名詞は正確に記載されている。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトルは PR の内容(リバートと restart オプションの改善)を適切に要約しており、一致している。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事は PR に記載された情報以外のバージョンサポートやリリース日程等の外部知識を追加していない。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

時間表現の歪曲はなく、PR の記述と整合している。