冒頭まとめ

CrashLoopBackOff は、コンテナが落ちた原因の名前ではありません。落ちたコンテナを再び起動するまで、待たせている状態の名前です。この文字列自体は、kubelet のソースに定義された固定の値で、それ以上の意味を持ちません。原因は別の場所、直前に終了したときの情報に残っています。

待ち時間の決まり方は、ソースに明確に書かれています。初回は10秒で、以降は倍々に増え、上限は300秒です。起点になるのは、直前に終了したコンテナの終了時刻です。

もう1つ、知っておくと症状の見え方が説明できる値があります。待ち時間の記録は、600秒つまり10分のあいだ更新されなければ期限切れになります。言い換えると、10分以上持ちこたえたコンテナは、次に落ちたとき10秒から数え直します。逆に、9分ごとに落ち続けるコンテナは記録が消えないため、待ち時間が上限に張り付いたままになります。「同じように落ちているのに、片方は復旧が速く、片方は遅い」という現象は、たいていこの境目です。

したがって、見るべきものは3つに絞れます。直前の終了理由、終了コード、そして直前の出力です。この3つが揃えば、CrashLoopBackOff という表示は読み終わったも同然です。

エラーの概要

一覧では、状態の欄にこの文字列が出ます。再起動の回数が増え続けるのが特徴です。

NAME                    READY   STATUS             RESTARTS      AGE
my-app-7d9f8b6c-xk2p9   0/1     CrashLoopBackOff   6 (2m11s ago) 14m

詳細を見ると、待ち時間を含む記録が出ます。文言はソースに定義されているとおりの形です。

Events:
  Type     Reason     Age                   From     Message
  ----     ------     ----                  ----     -------
  Warning  BackOff    2m11s (x14 over 12m)  kubelet  Back-off restarting failed container my-app in pod my-app-7d9f8b6c-xk2p9

Last State:     Terminated
  Reason:       Error
  Exit Code:    1
  Started:      Wed, 29 Jul 2026 10:20:31 +0900
  Finished:     Wed, 29 Jul 2026 10:20:33 +0900

読むべきはこの下半分です。終了の理由と終了コード、そして開始から終了までの秒数です。上の例では2秒で落ちているので、起動処理のどこかで失敗していると分かります。

終了コードには規則があります。信号で終了した場合、128にその番号を足した値になります。したがって 137 は9番、つまり強制終了の信号です。143 は15番、つまり終了要求の信号です。1などの小さい値は、ソフトウェア自身が返した値です。

まず最初に:直前の終了の情報を読む

第一に、終了の理由を見ます。Error ならソフトウェアが自分で終了しています。OOMKilled ならメモリの上限で殺されています。

第二に、終了コードを見ます。137 が出ていれば強制終了なので、メモリの上限か、外部からの停止を疑います。1や2であれば、ソフトウェア側の判断による終了です。

第三に、開始から終了までの秒数を見ます。数秒なら起動処理、数分以上なら動作中の失敗です。この違いで、見るべきログの場所が変わります。

第四に、直前の出力を取り出します。今動いているコンテナではなく、落ちたほうの出力です。

kubectl logs my-app-7d9f8b6c-xk2p9 --previous

この指定を忘れると、起動途中のコンテナの出力を見ることになり、肝心の失敗の記録が見えません。

よくある原因と解決手順

原因1:起動直後に自分で終了している

終了の理由が Error で、開始から終了までが数秒の場合です。ソフトウェアが起動処理のどこかで失敗し、自分で終了しています。

直前の出力に理由が書かれていることがほとんどです。設定ファイルが見つからない、必要な環境変数が設定されていない、接続先に到達できない、といった内容です。

Before(必要な値が渡っていない):

containers:
  - name: my-app
    image: example/my-app:1.0

After(必要な値を渡す):

containers:
  - name: my-app
    image: example/my-app:1.0
    env:
      - name: DATABASE_URL
        valueFrom:
          secretKeyRef:
            name: my-app-secret
            key: database-url

接続先に到達できないことが原因の場合、コンテナ側ではなく相手側の準備が整っていない可能性があります。この場合、起動の順序を制御するか、ソフトウェア側で接続を待つ作りにします。順序の制御だけに頼ると、相手が一時的に落ちたときに同じ状態へ戻ります。

原因2:メモリの上限で殺されている

終了の理由が OOMKilled、終了コードが 137 の場合です。ソフトウェアは正常に動いていたのに、確保できるメモリの上限を超えたために外から止められています。

まず、上限の設定と実際の使用量を確認します。

kubectl get pod my-app-7d9f8b6c-xk2p9 \
  -o jsonpath='{.spec.containers[*].resources}' | python3 -m json.tool
kubectl top pod my-app-7d9f8b6c-xk2p9

上限を上げるのが直接の対処ですが、上げる前に、使用量が時間とともに増え続けていないかを確認してください。増え続けている場合、上限を上げても落ちる時刻が遅くなるだけです。

Before(上限が実態に対して低い):

resources:
  limits:
    memory: "128Mi"

After(実測に基づいて設定する):

resources:
  requests:
    memory: "256Mi"
  limits:
    memory: "512Mi"

要求量と上限の両方を書くのが基本です。要求量を書かないと、配置の判断に使われる値が無いため、余裕の無い場所に置かれることがあります。

原因3:正常終了しているのに再起動している

終了コードが 0 なのに CrashLoopBackOff になる場合です。処理は成功しているのに、再起動の方針が常時再起動になっているために、終了するたびに起動し直されています。

一度きりの処理であれば、方針を変えるか、そもそも常駐しない種類の資源として定義するのが正しい形です。

spec:
  restartPolicy: OnFailure   # 失敗したときだけ再起動する

なお、常時再起動の方針では、この設定は使えません。用途に合った種類の資源を選んでください。

原因4:生存確認の設定が厳しすぎる

ソフトウェアは動いているのに、生存確認に失敗して外から止められている場合です。この場合、終了コードは信号によるもの、多くは 137 になります。

起動に時間がかかるソフトウェアで、確認の開始が早すぎるのが典型です。

Before(起動を待たずに確認を始める):

livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 3
  periodSeconds: 5

After(起動の完了を別の仕組みで待つ):

startupProbe:
  httpGet:
    path: /healthz
    port: 8080
  periodSeconds: 5
  failureThreshold: 30

livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  periodSeconds: 5

起動用の確認を併用すると、起動が完了するまで生存確認が始まりません。待ち時間を長く固定するより、実態に合わせやすくなります。

原因5:復旧が遅いのは待ち時間の仕組みによる

原因を直したのに、すぐには復旧しない場合です。前述のとおり、待ち時間は倍々に増えて上限が300秒なので、何度も落ちたあとでは次の起動まで最大5分待ちます。

直したことを確認したい場合は、対象を作り直すのが早い方法です。作り直せば待ち時間の記録も消えます。

kubectl delete pod my-app-7d9f8b6c-xk2p9

管理下にある資源であれば、削除しても自動的に作り直されます。この操作は、待ち時間を飛ばすためだけに行うものだと理解しておいてください。原因が直っていなければ、また同じ状態に戻ります。

補足:似ているが別のもの

イメージを取得できない場合は、別の表示になります。コンテナが起動する前の段階なので、直前の終了の情報も存在しません。取得の待ち時間にも同じく上限300秒の仕組みが使われています。

Error だけが表示されて再起動が増えない場合は、再起動の方針が常時ではない可能性があります。Completed は正常終了で、再起動の方針が常時でなければこの表示のまま止まります。

OOMKilled が状態の欄に出ている場合は、まだ待ち時間に入っていない段階です(Kubernetes の OOMKilled の記事)。繰り返せば CrashLoopBackOff に変わります。同じ原因の、時間軸での見え方の違いです。

配置そのものができていない場合は、コンテナが起動していないので別の表示になります。資源の空きが足りない、条件に合う配置先が無い、といった状況です。

利用者向けの通信が失敗している場合は、状態コードとして現れます。コンテナが落ちている最中は、転送先が居ないため 503、居るのに通じない場合は 502 になります(Kubernetes の 503 の記事502 の記事)。

切り分けの順序

  1. 直前の終了の情報を見る。理由と終了コード、開始から終了までの秒数の3つを揃える。
  2. 直前の出力を取り出す。今のコンテナではなく、落ちたほうを指定する。
  3. 数秒で落ちているなら起動処理、数分以上なら動作中の失敗として、見る場所を切り替える。
  4. 終了コードが 137 なら、メモリの上限か生存確認による停止を疑う。理由の欄で区別できる。
  5. 終了コードが 0 なら、再起動の方針と用途が合っていない。
  6. 直したのに復旧しないのは待ち時間の仕組みによるもの。対象を作り直せば記録も消える。
  7. 10分以上持ちこたえれば待ち時間は初期値に戻る。落ちる間隔がこれより短いかどうかで、症状の見え方が変わる。

確認コマンド集

# 1. 直前に終了したコンテナの出力を見る(最重要)
kubectl logs <ポッド名> --previous

# 2. 直前の終了の理由と終了コードを取り出す
kubectl get pod <ポッド名> \
  -o jsonpath='{range .status.containerStatuses[*]}{.name}{"  reason="}{.lastState.terminated.reason}{"  exit="}{.lastState.terminated.exitCode}{"\n"}{end}'

# 3. 待ち時間を含む記録を見る
kubectl describe pod <ポッド名> | sed -n '/Events:/,$p'

# 4. 上限の設定と実際の使用量を比べる
kubectl get pod <ポッド名> -o jsonpath='{.spec.containers[*].resources}' | python3 -m json.tool
kubectl top pod <ポッド名>

# 5. 起動から終了までの秒数を確認する
kubectl get pod <ポッド名> \
  -o jsonpath='{.status.containerStatuses[0].lastState.terminated.startedAt}{"  -> "}{.status.containerStatuses[0].lastState.terminated.finishedAt}{"\n"}'

# 6. 待ち時間を飛ばして、直したかどうかを早く確かめる
kubectl delete pod <ポッド名>

Editor’s Note

本文で挙げた「上限300秒」と「10分で記録が消える」という2つの数字は、無関係ではありません。改善提案の文書に、記録が消えるまでの時間は上限の2倍として計算されている、と書かれています。300秒の2倍で600秒、つまり10分です。片方を変えればもう片方も動く関係になっていた、ということです。

この2つの値は、長く議論の対象になってきました(Tune CrashLoopBackOff)。提案の文書には、対応する要望が250を超える賛同を集めており、本体の要望の中で上位5件に入る、と記されています。そして、5分まで伸びる待ち時間も、10分という復旧の条件も、どちらも慎重すぎるという評価が書かれています。特に問題視されているのは、終了コードが 0 で正常に終わった場合と、そもそも10分より短く動く前提の場合です。本記事の原因3で触れた形が、まさにこれに当たります。

現在は、待ち時間を短くする変更が試験的な機能として入っています。初回1秒、上限1分という値で、専用の切り替えで有効にする形です。文書には、試験期間中にさらに調べたうえで、初回をより0に近づけ、上限を10秒から30秒あたりに置くことも検討する、と書かれています。また、上限を運用側で設定できるようにする部分は、別の提案として切り出されています。

つまり、いま目にしている待ち時間は、変わる可能性のある値です。それでも、記録が消える条件が上限の2倍だという関係と、落ちる間隔がその境目をまたぐかどうかで見え方が変わることは、値が変わっても残ります。数字そのものより、この構造を覚えておいてください。


免責事項:本記事の内容は、執筆時点の公開情報をもとに作成したものです。ソフトウェアの仕様は予告なく変更されることがあります。最新の情報は各ツールの公式サポートページをご確認ください。本記事の情報を利用した結果生じたいかなる損害についても、著者および運営者は責任を負いかねます。