できない.dev

Docker の HEALTHCHECK がいつも unhealthy になる

HEALTHCHECK は CMD が exit 0 で healthy、それ以外で unhealthy。
コンテナに curl 等が無い、起動待ち時間不足、shell / exec 形式の書き分けが主因。
docker inspect の Health.Log で実エラーを確認する。

公開: 更新:

実行例あり(2026-09-23 に実環境で検証)

要約

Docker の HEALTHCHECK は指定した CMD が exit 0 を返したときだけ healthy、それ以外は unhealthy として扱われる。
常に unhealthy になるときは、まず docker inspect でログを確認し、CMD の中身(コマンドの存在、ポート、exit code)と --start-period の値を順に疑う。

実行例

curl の入っていない alpine イメージに HEALTHCHECK CMD curl -f を書いたコンテナは、起動から十数秒で unhealthy に落ち、Health.Log には exit code 1 と /bin/sh: curl: not found だけが並ぶ。
同じイメージでチェックコマンドを wget --spider に替えた側は、同じ間隔で healthy を保っている。

$ docker run -d --name hc-broken hc-broken
0814bb02513414eaebe0e48bc00db6cca9f8b06333354bda5ea956405b220fa3
$ docker ps --filter name=hc-broken --format "{{.Names}}\t{{.Status}}"
hc-broken	Up 12 seconds (unhealthy)
$ docker inspect --format "{{json .State.Health}}" hc-broken
{"Status":"unhealthy","FailingStreak":4,"Log":[{"Start":"2026-09-23T03:23:02.443108334Z","End":"2026-09-23T03:23:02.537148116Z","ExitCode":1,"Output":"/bin/sh: curl: not found\n"},{"Start":"2026-09-23T03:23:05.538550243Z","End":"2026-09-23T03:23:05.606169899Z","ExitCode":1,"Output":"/bin/sh: curl: not found\n"},{"Start":"2026-09-23T03:23:08.607148303Z","End":"2026-09-23T03:23:08.653276148Z","ExitCode":1,"Output":"/bin/sh: curl: not found\n"},{"Start":"2026-09-23T03:23:11.654363954Z","End":"2026-09-23T03:23:11.697671272Z","ExitCode":1,"Output":"/bin/sh: curl: not found\n"}]}
$ docker run -d --name hc-fixed hc-fixed
17b19108f823e4e055ec7bedf6d987d0e5e747bc25427e0fd4ccb9cde9b8cc85
$ docker ps --filter name=hc-fixed --format "{{.Names}}\t{{.Status}}"
hc-fixed	Up 12 seconds (healthy)
$ docker inspect --format "{{json .State.Health}}" hc-fixed
{"Status":"healthy","FailingStreak":0,"Log":[{"Start":"2026-09-23T03:23:16.919672761Z","End":"2026-09-23T03:23:16.957757845Z","ExitCode":0,"Output":"Connecting to localhost:8080 ([::1]:8080)\nremote file exists\n"},{"Start":"2026-09-23T03:23:19.958671062Z","End":"2026-09-23T03:23:19.997874017Z","ExitCode":0,"Output":"Connecting to localhost:8080 ([::1]:8080)\nremote file exists\n"},{"Start":"2026-09-23T03:23:22.999018986Z","End":"2026-09-23T03:23:23.046336483Z","ExitCode":0,"Output":"Connecting to localhost:8080 ([::1]:8080)\nremote file exists\n"},{"Start":"2026-09-23T03:23:26.04600454Z","End":"2026-09-23T03:23:26.09002426Z","ExitCode":0,"Output":"Connecting to localhost:8080 ([::1]:8080)\nremote file exists\n"}]}

— 2026-09-23 時点の出力

検証環境

検証日
実行環境
local host (Windows 11 Home, Docker 29.1.3)
バージョン
  • Node.js 22.14.0
  • npm 10.9.2
  • Git 2.48.1.windows.1
  • Docker 29.1.3

この記事の「実行例」は、上記の環境で実際にコマンドを実行して得られた出力をそのまま掲載しています。 再現手順はリポジトリの検証スクリプトとして管理し、定期的に再実行して出力を更新しています。

よくある原因

  1. CMD が exit 0 を返していない: 期待する 200 が返らない、ホスト名解決に失敗している、認証が必要になっている、など意外と外的要因が多い。
  2. curl / wget が無い: alpine ベースのイメージは既定で curl を含まないため、HEALTHCHECK CMD curl -f ... が即 not found で落ちる。
  3. start-period 不足: アプリが起動完了する前から check が始まり、初期化中に何度も失敗扱いされる。
  4. 書式の罠: shell 形式 HEALTHCHECK CMD curl -f http://localhost/ と exec 形式 HEALTHCHECK CMD ["curl", "-f", "http://localhost/"] で挙動が変わる。
    リダイレクトやパイプを使うなら shell 形式が必要。

解決策

1. まず Health.Log を見る

docker inspect --format '{{json .State.Health}}' <container>

Log 配列に直近 5 件の ExitCode と Output が入っているので、CMD が何を出力したかを直接確認する。
詳細は docker inspect 公式(新しいタブで開く) を参照。

2. curl が無いイメージへの対応

# alpine の場合
RUN apk add --no-cache curl
 
# あるいは wget で代替
HEALTHCHECK CMD wget --no-verbose --tries=1 --spider http://localhost:8080/ || exit 1

wget も無いミニマムイメージなら、Node や Python など同梱済みの言語で軽い HTTP リクエストを 1 行書く方法もある。

3. start-period を伸ばす

HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
  CMD curl -f http://localhost:8080/health || exit 1

--start-period の間は失敗してもカウントされず、起動に時間のかかるアプリで unhealthy 確定を防げる。
各オプションは HEALTHCHECK 公式リファレンス(新しいタブで開く) のとおり。

4. exec 形式で曖昧さを除く

HEALTHCHECK CMD ["curl", "-f", "http://localhost:8080/health"]

shell の解釈が挟まらないため、変数展開・パイプを使わないなら exec 形式が安全。|| exit 1 を入れるなら shell 形式に戻す必要がある。

この記事は役立ちましたか?