Docker Compose の healthcheck を理解しよう|“起動した”と“使える”は違う

こんにちは、さるまりんです 🐒🔧

Docker Composeを使っていると、複数のコンテナをまとめて立ち上げられて便利ですよね。

たとえば、

  • アプリケーション
  • データベース
  • Redis
  • メール確認用のツール
  • APIモック

などを、ひとつの docker compose up で起動できるようにしておくと、開発環境をかなり再現しやすくなります。

最近は、開発環境をDockerの中に閉じ込めて、できるだけ本番環境に近い形で動かすことも増えてきました。

ただ、ここでひとつ気をつけたいことがあります。

それは、

「コンテナが起動した」ことと「そのサービスが使える状態になった」ことは同じではない

ということです。

今回は、Docker Composeの healthcheck を使って、コンテナの「使える状態」をどう考えるかを整理してみたいと思います。


■ コンテナが起動しても、まだ使えないことがある

Docker Composeでは、複数のサービスを定義できます。

たとえば、アプリケーションとPostgreSQLを使う場合、ざっくり次のような構成になります。

services:
  app:
    image: alpine
    depends_on:
      - db

  db:
    image: postgres:16

このように depends_on を書くと、app より先に db を起動するようにできます。

一見これで大丈夫そうに見えます。

でも、実際にはそうとは限らないのです。

db コンテナが起動していても、PostgreSQLがまだ接続を受け付けられる状態になっていないことがあります。

つまり、

dbコンテナは起動した
でも、DBとしてはまだ使えない

という状態がありえます。

このタイミングでアプリケーションがDBへ接続しようとすると、接続エラーになることがあります。

Ready or Not Ready。

この状態がわからずに、困ったことがありました。


■ depends_on は「起動順」を見るもの

depends_on は便利ですが、基本的には「起動する順番」を扱うものです。

たとえば、先ほどと同じように書いた場合です。

services:
  app:
    image: alpine
    depends_on:
      - db

  db:
    image: postgres:16

この場合、Composeは db を先に起動してから app を起動します。

ただし、ここで見ているのはあくまでコンテナの起動です。

db のコンテナが上がったら、app が起動されます。

でも、db の中にあるDBの状態までは見ていません。

DBの初期化が終わった?
接続できる状態になった?
必要な準備が完了した?

そこまでは、単純な depends_on だけでは見てくれません。

ちょっとややこしいですね。

開発環境ではたまたま動いていても、マシンの状態や初回起動のタイミングによって、たまに失敗することがあります。

「昨日は動いたのに、今日は起動直後だけ失敗する」

みたいなことが起きると、原因がよくわからずに困ってしまうことがあるんですね。


■ healthcheck で「元気かどうか」を見る

そこで使えるのが healthcheck です。

healthcheck は、コンテナが正常な状態かどうかを確認するための仕組みです。

たとえばPostgreSQLなら、pg_isready というコマンドを使って、DBが接続可能な状態かを確認できます。

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app_db
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app_db"]
      interval: 5s
      timeout: 3s
      retries: 5

ここでは、db サービスに healthcheck を追加しています。

それぞれの意味は、ざっくり次のような感じです。

項目 意味
test 実行する確認コマンド
interval 何秒ごとに確認するか
timeout 確認コマンドを何秒でタイムアウトするか
retries 何回失敗したらunhealthyとみなすか

この例では、5秒ごとに pg_isready を実行し、3秒以内に応答がなければ失敗とします。

そして、5回失敗したら、そのコンテナは正常ではないと判断します。

なお、healthcheck の確認コマンドは、基本的に終了コードで判断されます。

終了コードが 0 なら成功。
それ以外なら失敗。

そのため、自分でコマンドを書くときは「正常なら0で終わるか」を少し気にしておくと安心です。


■ service_healthy を使って、使える状態になるまで待つ

healthcheck を書いただけでも、コンテナの状態を確認できるようになります。

あともう一つ進んで、アプリケーション側を「DBがhealthyになるまで待たせたい」場合は、depends_on と組み合わせます。

services:
  app:
    image: alpine
    depends_on:
      db:
        condition: service_healthy
    command: ["sh", "-c", "echo DB is ready!"]

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app_db
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app_db"]
      interval: 5s
      timeout: 3s
      retries: 5

ポイントはここです。

depends_on:
  db:
    condition: service_healthy

これにより、appdbhealthy になるまで待ってから起動されます。

つまり、

dbコンテナが起動したらappを起動する

ではなく、

dbが使える状態になったらappを起動する

に近づけることができます。

この違いは、実際に使っているとけっこう大きいんです。


■ 実際に試してみる

簡単な compose.yaml を作って試してみます。

services:
  app:
    image: alpine
    depends_on:
      db:
        condition: service_healthy
    command: ["sh", "-c", "echo DB is ready!"]

  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app_db
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app_db"]
      interval: 5s
      timeout: 3s
      retries: 5

このファイルを用意したら、次のコマンドで起動します。

docker compose up

起動すると、まず db が立ち上がります。

その後、healthcheck によってPostgreSQLが使える状態かどうかが確認されます。

dbhealthy になると、app が起動して次のように表示されます。

DB is ready!

小さな例ですが、

起動順ではなく、使える状態を待つ

という感覚がつかめるのではないでしょうか。


■ 状態は docker compose ps でも確認できる

コンテナの状態は、別のターミナルから次のコマンドでも確認できます。

docker compose ps

healthcheck が設定されていると、状態に healthyunhealthy が表示されます。

たとえば、正常であればこのようなイメージです。

NAME      IMAGE         STATUS
db        postgres:16   Up 10 seconds (healthy)

この表示を見ると、単に Up になっているだけでなく、healthy になっていることがわかります。


■ healthcheck は何を書けばいい?

healthcheck の中では、そのサービスが本当に使えるかを確認できるコマンドを書きましょう。

たとえば、PostgreSQLなら先ほどのように pg_isready を使えます。

healthcheck:
  test: ["CMD-SHELL", "pg_isready -U app -d app_db"]

MySQLなら、次のように mysqladmin ping を使うこともできます。

healthcheck:
  test: ["CMD-SHELL", "mysqladmin ping -h localhost -u root -psecret"]

Webアプリなら、curl でヘルスチェック用のURLを見ることもあります。

healthcheck:
  test: ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]

大事なのは、

そのサービスが本当に使えるかを確認する

ということです。

単にプロセスがあるかどうかだけでは、十分ではないこともあります。


■ 何でもhealthcheckに詰め込めばいいわけではない

便利な healthcheck ですが、何でも入れればよいというものでもありません。

たとえば、次のような確認を入れすぎると、逆に扱いにくくなることがあります。

  • 外部APIにつながるか
  • 大量のデータが存在するか
  • 他の複数サービスが全部正常か
  • 重い処理が最後まで通るか

もちろん必要な場合もあります。

ただ、開発環境の healthcheck では、まずはシンプルに、

このコンテナのサービスが最低限使える状態か

を見るくらいから始めるのがよさそうです。

DBなら接続できるか。
WebならヘルスチェックURLが返るか。
Redisなら PING に応答するか。

このくらいの粒度が扱いやすいと思います。

分離することが大切な場合もあります。

外部サービスのメンテナンスに開発環境が引っ張られて、作業が進まない。

そんなことは、できれば避けたいですよね。


■ healthcheckだけに頼りすぎない

これも大事です。

healthcheck を書くと、起動順は安定してきます。

でも、アプリケーション側のリトライ設計が不要になるわけではありません。

たとえば本番環境では、DBや外部サービスが一時的に落ちることもあります。

そのときにアプリケーションが一度の接続失敗ですぐ終了してしまうと、どうでしょう。

起動時だけうまく待てても、動いている途中の失敗には弱いままです。

Docker Composeの healthcheck は、あくまで開発環境やコンテナ起動時の安定性を高めるための仕組みです。

本当に安定した構成にするには、

  • アプリケーション側でリトライする
  • エラー時のログを残す
  • 起動時だけでなく、動いている途中の失敗も考える

といった設計も必要です。

ここは、Dockerだけで全部解決しようとしない方がよいところです。


■ AIにDocker Composeを書いてもらう時にも役立つ

最近は、ChatGPTなどに compose.yaml を作ってもらうこともあります。

それ自体はとても便利です。

ただ、出てきた設定をそのまま使うだけだと、

depends_on:
  - db

だけで終わっていることもあります。

もちろん、それで十分な場合もあります。

でも、DBの準備が終わる前にアプリが起動してしまうような構成では、healthcheck があるかどうかで安定感が変わってきます。

AIに作ってもらった設定でも、

  • 起動順だけを見ているのか
  • 使える状態まで待っているのか
  • アプリ側のリトライはあるのか

を自分で確認できると、かなり安心です。

AIを使うほど、こういう基本の理解が大事になってくる気がします。

AIが書いてくれたコードをすべて読むのは大変かもしれません。

でも、大切なところはちゃんと読んでおかないと、困ったことになることもありますね。


■ まとめ

Docker Composeの healthcheck について整理してみました。

今回のポイントは、次のとおりです。

  • コンテナが起動していても、サービスが使えるとは限らない
  • depends_on だけでは、基本的に起動順を見る
  • healthcheck を使うと、コンテナの状態を確認できる
  • condition: service_healthy と組み合わせると、依存先がhealthyになるまで待てる
  • ただし、アプリケーション側のリトライ設計も大切

Dockerで開発環境を閉じ込めると、環境の再現性はかなり上がります。

でも、その中で動くサービス同士にも「準備のタイミング」があります。

起動したか。
本当に使えるか。
失敗したときにどうするか。

このあたりを意識できると、Docker Composeの設定も、ただの起動手順ではなく、開発環境を安定させるための設計として見えてくるのではないでしょうか。

小さな healthcheck ですが、複数コンテナの環境を扱うときには、ありがたい仕組みですね。

読んでくださってありがとうございました。

それではまた!