🫥

Docker Composeでバックエンドコンテナがunhealthyに?原因は意外な「あのコマンド」の不在だった!【生成AI執筆】

に公開

Docker Composeを使って開発環境を構築していると、時々コンテナが期待通りに起動せず、「unhealthy(不健康)」と表示されてしまうことがあります。特にバックエンドサービスがデータベースなどの他のサービスに依存している場合、この問題は連鎖的に他のコンテナの起動失敗を引き起こし、頭を抱える原因となりがちです。

先日、まさにこの「unhealthy」問題に遭遇し、試行錯誤の末に解決しました。今回はその経緯と解決策を、同じように困っている方々の助けになればと思い、記事にまとめます。

問題発生:バックエンドコンテナが起動しない!

開発中のプロジェクトで docker compose up -d を実行したところ、以下のような出力と共にバックエンドコンテナの起動に失敗しました。

manntera@DESKTOP-5GE6CLF:~/works/product/enbis$ docker compose up -d
[+] Running 4/4
 ✔ Network enbis_default        Created                                                                                               0.3s
 ✔ Container enbis-db-1         Healthy                                                                                               6.3s
 ✘ Container enbis-backend-1    Error                                                                                                57.8s
 ✔ Container enbis-frontend-1   Created                                                                                               0.1s
dependency failed to start: container enbis-backend-1 is unhealthy

enbis-backend-1 コンテナが Error となり、最終的に unhealthy と判断されています。そして、このバックエンドに依存している enbis-frontend-1 コンテナも起動できていません(Created のまま)。

私の docker-compose.yml における backend サービスの設定は以下のようになっており、一般的なヘルスチェック (curl を使用) を設定していました。

# docker-compose.yml (抜粋)
services:
  # ... (dbサービスなど)

  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    # ... (environment, ports, depends_onなど)
    restart: always
    healthcheck:
      test: [ "CMD", "curl", "-f", "http://localhost:5555/health" ] # ← 問題のヘルスチェック
      interval: 10s
      timeout: 5s
      retries: 3
      start_period: 30s # 当初の設定

  frontend:
    # ...
    depends_on:
      backend:
        condition: service_healthy # backendがhealthyになるのを待つ
    # ...

バックエンドアプリケーション(Node.js + Hono)のログを確認すると (docker compose logs backend)、一見サーバーは正常に起動しているように見えました。

backend-1  | > start
backend-1  | > node dist/index.cjs
backend-1  |
backend-1  | Server is running on port 5555

さらにややこしいことに、ホストOSのブラウザから http://localhost:5555/health にアクセスすると、ちゃんと "OK" という文字が返ってきていたのです。

原因調査の道のり

「サーバーは動いてるっぽいのに、なぜ unhealthy なんだ…?」と、ここから原因調査が始まりました。

ステップ1:ヘルスチェック用エンドポイントのログ確認

バックエンドの /health エンドポイントには、アクセスがあった際にコンソールにログを出すようにしていました。

// backend/src/entry_point.ts (抜粋)
app.get('/health', (c) => {
    console.log('Health check endpoint accessed'); // このログが出るはず!
    return c.text('OK', 200);
});

しかし、docker compose logs backend の出力を見ても、この Health check endpoint accessed というログは一向に現れませんでした。これは、ヘルスチェックの curl コマンドがアプリケーションの該当エンドポイントに到達していない可能性を示唆していました。

ステップ2:start_period の調整

アプリケーションの起動やマイグレーションに時間がかかり、ヘルスチェックが始まる前にタイムアウトしている可能性を考えました。そこで、docker-compose.ymlhealthcheck にある start_period(ヘルスチェックの失敗を許容する猶予期間)を 30s から 60s、さらには 120s へと延ばしてみましたが、状況は変わりませんでした。

ステップ3:コンテナ内部での直接確認(これが決め手に!)

いよいよコンテナ内部で何が起きているのかを直接確認することにしました。

  1. まず、docker-compose.ymlbackend サービスの healthcheck セクションを一時的にコメントアウトし、コンテナが unhealthy で停止しないようにしました。
  2. docker compose down && docker compose up -d --build backend でバックエンドコンテナだけを再起動。
  3. docker compose exec backend sh コマンドで、起動したコンテナのシェルに入りました。

そして、コンテナ内部でヘルスチェックに使われている curl コマンドの状態を確認してみると…

# (コンテナ内部のシェルで)
# curl --version
sh: 4: curl: not found

「sh: 4: curl: not found」

なんと、コンテナ内に curl コマンドが存在していなかったのです!

原因判明:ヘルスチェックに必要な curl がコンテナイメージになかった

私のバックエンドコンテナは node:slim という軽量なNode.jsイメージをベースにしていました。この slim イメージは、容量を小さくするために多くの共通ツール(curl も含む)がデフォルトではインストールされていません。

ヘルスチェックに curl を指定していたものの、その実行ファイルが存在しないため、ヘルスチェックコマンド自体が失敗し、Dockerはコンテナを unhealthy と判断していたのです。ホストOSのブラウザからアクセスできたのは、ポートフォワーディングは正常で、コンテナ内のアプリケーション自体は(curl がなくても)起動していたためでした。

解決策:Dockerfileに curl のインストールを追加

原因が判明すれば解決は簡単です。backend/Dockerfilecurl をインストールするコマンドを追加しました。

# backend/Dockerfile

# (前略: ビルダーステージなど)

FROM node:slim@sha256:dfb18d8011c0b3a112214a32e772d9c6752131ffee512e974e59367e46fcee52
WORKDIR /app
ENV NODE_ENV=production \
    PORT=5555

# ======== ↓↓↓ ここを追加! ========
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
# ======== ↑↑↑ ここまで ========

COPY --from=builder /app/node_modules ./node_modules
# (中略: 他のCOPY命令)
COPY package.json package-lock.json* ./

CMD ["sh", "-c", "npm run migrations:push && npm start"]

EXPOSE 5555

そして、docker-compose.ymlhealthcheck 設定のコメントアウトを解除し、再度イメージをビルドしてコンテナを起動しました。

docker compose down
docker compose up -d --build

すると…

manntera@DESKTOP-5GE6CLF:~/works/product/enbis$ docker compose ps
NAME                 IMAGE               COMMAND                  SERVICE             CREATED             STATUS              PORTS
enbis-backend-1      enbis-backend       "sh -c 'npm run mig…"   backend             About a minute ago  Up About a minute (healthy)   0.0.0.0:5555->5555/tcp
enbis-db-1           postgres:15         "docker-entrypoint.s…"   db                  About a minute ago  Up About a minute (healthy)   0.0.0.0:5432->5432/tcp
enbis-frontend-1     enbis-frontend      "/docker-entrypoint.…"   frontend            About a minute ago  Up About a minute             0.0.0.0:3000->80/tcp

ついに enbis-backend-1healthy に! そして、それに依存する enbis-frontend-1 も無事に起動しました。

まとめと教訓

今回の件から得られた教訓は以下の通りです。

  • unhealthy の原因は様々だが、ヘルスチェックコマンド自体がコンテナ内に存在するかは基本中の基本。
  • xxx:slimxxx:alpine のような軽量イメージを使う際は、必要なツールがデフォルトで含まれていない可能性を常に意識する。curl だけでなく、wget, bash, git なども同様です)
  • 問題の切り分けとして、コンテナ内部に入って手動でコマンドを実行してみるのは非常に有効な手段。 ログだけでは見えない問題を発見できます。
  • ホストOSからアクセスできることと、コンテナ内部から(特に localhost や他のサービスへ)アクセスできることは、必ずしもイコールではない。

もしあなたがDocker Composeでコンテナの unhealthy 問題に直面したら、アプリケーションのバグやリソース不足を疑う前に、まずはヘルスチェックに使っているコマンドがコンテナ内にちゃんと存在するかを確認してみてください。意外とあっさり解決するかもしれません。

この記事が、同じような問題で悩む誰かの一助となれば幸いです。

Discussion