👏

Docker Compose で依存先のコンテナが「ちゃんと」起動しているかチェックするひとつの方法

2023/05/12に公開

背景

Docker Compose を使って複数のコンテナをまとめる場合、「あるコンテナが起動してから別のコンテナを起動する」、といったようにコンテナを決まった順番に起動させたい場合がよくあると思います。

  • DBコンテナが起動してからAPサーバを起動したい
  • モックサーバが起動してからAPIサーバを起動したい などなど

そんな場合によく使われるのが Docker Compose のdepends_onですが、実はこのオプションも独りではそこまで万能でもありません。

公式の記述を引用します。

example-compose.yml
version: "3.9"
services:
  web:
    build: .
    depends_on:
      - db
      - redis
  redis:
    image: redis
  db:
    image: postgres

depends_on では、 web を開始する前に db と redis の「準備」が整うのを待ちません。単に、順番通り開始するだけです。

というわけで、素のdepends_onで指定できるのは単にコンテナの「起動順」のみであり、依存先のコンテナで必要なサービスが起動しているかまではチェックしてくれていないことがわかります。

上記で挙げた「APサーバとDBサーバ」のような構成の場合、おおむねDBサーバが必要とされるのはAPサーバがリクエストを受けるタイミングなので、起動時にはそこまで問題とならないことが多いですが、起動直後から別の依存先のサービスを参照するようなアプリケーションの場合、起動に失敗してしまうこともあります。

ひとつの攻略例

ここでは、依存先のサービスがpostgresqlである場合を例にとり、postgresqlがちゃんと起動してから他のコンテナを起動する例をご紹介します。

うまくいかない例

compose.yml
services:
  postgres:
    image: postgres:14
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password    
      POSTGRES_DB: ferretdb
    ports:
      - 5432:5432
    volumes: 
      - postgres-bg-ferretdb:/var/lib/postgresql/data
    networks:
      - app-network

  ferretdb:
    image: ghcr.io/ferretdb/ferretdb
    ports:
      - 27017:27017
    environment:
      FERRETDB_POSTGRESQL_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/ferretdb
    networks:
      - app-network
    depends_on:
      - postgres  # postgres に依存している

volumes:
  postgres-bg-ferretdb:

networks:
  app-network:

postgresql に依存するコンテナとしては「FerretDB」を使ってみました。
FerretDB は バックエンドに postgresql のようなRDBMSを用いていて、起動時にバックエンドのDBへ接続するため、今回の場合は postgresql のサービスが起動していないと、起動に失敗します。

この状態で、docker compose up -dしてみます。

docker compose up -dの結果

ステータスだけだとうまく起動しているようにも見えますので、docker compose logs -f ferretdbとして、ferretdbの状態を確認してみます。

docker compose logs -f ferretdb
ferretdb-ferretdb-1  | 2023-05-11T08:46:33.313+0900 INFO  erretdb/main.go:231 Starting FerretDB v1.0.0... {"version": "v1.0.0", "commit": 

--- 途中省略 ---

ferretdb-ferretdb-1  | 2023-05-11T08:46:34.248+0900 INFO  pgdb  v4@v4.18.1/conn.go:354  Dialing PostgreSQL server {"host": "postgres"}
ferretdb-ferretdb-1  | 2023-05-11T08:46:34.250+0900 ERROR pgdb  v4@v4.18.1/conn.go:354  connect failed  {"err": "failed to connect to `host=postgres user=postgres database=ferretdb`: dial error (dial tcp 172.20.0.3:5432: connect: connection refused)"}
ferretdb-ferretdb-1  | github.com/jackc/pgx/v4.(*Conn).log
ferretdb-ferretdb-1  |  /cache/gomodcache/github.com/jackc/pgx/v4@v4.18.1/conn.go:354
ferretdb-ferretdb-1  | github.com/jackc/pgx/v4.connect
ferretdb-ferretdb-1  |  /cache/gomodcache/github.com/jackc/pgx/v4@v4.18.1/conn.go:225
ferretdb-ferretdb-1  | github.com/jackc/pgx/v4.ConnectConfig
ferretdb-ferretdb-1  |  /cache/gomodcache/github.com/jackc/pgx/v4@v4.18.1/conn.go:113
ferretdb-ferretdb-1  | github.com/jackc/pgx/v4/pgxpool.ConnectConfig.func1
ferretdb-ferretdb-1  |  /cache/gomodcache/github.com/jackc/pgx/v4@v4.18.1/pgxpool/pool.go:232
ferretdb-ferretdb-1  | github.com/jackc/puddle.(*Pool).constructResourceValue
ferretdb-ferretdb-1  |  /cache/gomodcache/github.com/jackc/puddle@v1.3.0/pool.go:558
ferretdb-ferretdb-1  | github.com/jackc/puddle.(*Pool).Acquire.func1
ferretdb-ferretdb-1  |  /cache/gomodcache/github.com/jackc/puddle@v1.3.0/pool.go:317
ferretdb-ferretdb-1  | 2023-05-11T08:46:34.250+0900 WARN  pg/pg.go:136  DBPool: authentication failed {"username": "", "error": "pgdb.NewPool: failed to connect to `host=postgres user=postgres database=ferretdb`: dial error (dial tcp 172.20.0.3:5432: connect: connection refused)"}

--- 途中省略 ---

ferretdb-ferretdb-1  | 2023-05-11T08:46:34.250+0900 ERROR // 172.20.0.5:46074 -> 172.20.0.4:27017   clientconn/conn.go:556  Response message:
ferretdb-ferretdb-1  | {
ferretdb-ferretdb-1  |   "Checksum": 0,
ferretdb-ferretdb-1  |   "FlagBits": 0,
ferretdb-ferretdb-1  |   "Sections": [
ferretdb-ferretdb-1  |     {
ferretdb-ferretdb-1  |       "Document": {
ferretdb-ferretdb-1  |         "$k": [
ferretdb-ferretdb-1  |           "ok",
ferretdb-ferretdb-1  |           "errmsg",
ferretdb-ferretdb-1  |           "code",
ferretdb-ferretdb-1  |           "codeName"
ferretdb-ferretdb-1  |         ],
ferretdb-ferretdb-1  |         "ok": {
ferretdb-ferretdb-1  |           "$f": 0
ferretdb-ferretdb-1  |         },
ferretdb-ferretdb-1  |         "errmsg": "[msg_listdatabases.go:34 pg.(*Handler).MsgListDatabases] [pg.go:137 pg.(*Handler).DBPool] pgdb.NewPool: failed to connect to `host=postgres user=postgres database=ferretdb`: dial error (dial tcp 172.20.0.3:5432: connect: connection refused)",
ferretdb-ferretdb-1  |         "code": 1,
ferretdb-ferretdb-1  |         "codeName": "InternalError"
ferretdb-ferretdb-1  |       },
ferretdb-ferretdb-1  |       "Kind": 0
ferretdb-ferretdb-1  |     }
ferretdb-ferretdb-1  |   ]
ferretdb-ferretdb-1  | }
ferretdb-ferretdb-1  |
ferretdb-ferretdb-1  |
ferretdb-ferretdb-1  |
ferretdb-ferretdb-1  | github.com/FerretDB/FerretDB/internal/clientconn.(*conn).logResponse
ferretdb-ferretdb-1  |  /src/internal/clientconn/conn.go:556
ferretdb-ferretdb-1  | github.com/FerretDB/FerretDB/internal/clientconn.(*conn).run
ferretdb-ferretdb-1  |  /src/internal/clientconn/conn.go:293
ferretdb-ferretdb-1  | github.com/FerretDB/FerretDB/internal/clientconn.acceptLoop.func1
ferretdb-ferretdb-1  |  /src/internal/clientconn/listener.go:307
ferretdb-ferretdb-1  | 2023-05-11T08:46:34.250+0900 WARN  listener  clientconn/listener.go:311  Connection stopped  {"conn": "172.20.0.5:46074 -> 172.20.0.4:27017", "error": "fatal error"}
ferretdb-ferretdb-1  | 2023-05-11T08:46:35.258+0900 INFO  listener  clientconn/listener.go:305  Connection started  {"conn": "172.20.0.5:46078 -> 172.20.0.4:27017"}
ferretdb-ferretdb-1  | 2023-05-11T08:46:35.395+0900 INFO  listener  clientconn/listener.go:305  Connection started  {"conn": "172.20.0.2:37696 -> 172.20.0.4:27017"}
ferretdb-ferretdb-1  | 2023-05-11T08:46:35.504+0900 INFO  listener  clientconn/listener.go:305  Connection started  {"conn": "172.20.0.2:37704 -> 172.20.0.4:27017"}

dial error (dial tcp 172.20.0.3:5432: connect: connection refused)が出力されており、postgresqlへの接続に失敗していることがわかります。[1]
その後、リトライによりConnection startedとなって正常に起動できている様子も見えますが、なんとなくきもちわるいのでどうにかしたいところです。

うまくいくようにしてみる

compose.yml
services:
  postgres:
    image: postgres:14
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password    
      POSTGRES_DB: ferretdb
    ports:
      - 5432:5432
    volumes: 
      - postgres-data:/var/lib/postgresql/data
+    healthcheck:  # コンテナの起動チェック
+      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres}"]
+      interval: 5s
+      retries: 3
    networks:
      - app-network

  ferretdb:
    image: ghcr.io/ferretdb/ferretdb
    ports:
      - 27017:27017
    environment:
      FERRETDB_POSTGRESQL_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/ferretdb
    networks:
      - app-network
    depends_on:
-      - postgres
+      postgres:
+        condition: service_healthy  # postgres がちゃんと起動したら start

volumes:
  postgres-data:

networks:
  app-network:

一度docker compose downしたあと、同様にdocker compose up -dしてみます。

再度up

upコマンド実行直後の状態です。postgresコンテナがWaitingとなり、ferretdbコンテナはCreatedの状態で待っている様子がわかります。

started

数秒待つと、postgresコンテナの状態がHealthyとなり、ferretdbStartedとなりました。

docker compose logs -f ferretdb
ferretdb-ferretdb-1  | 2023-05-11T09:12:36.833+0900 INFO  ferretdb/main.go:231  Starting FerretDB v1.0.0... {"version": "v1.0.0", "commit": "6734769da718c9b1b182f9c4ab61fb5e9fa37bd6", "branch": "unknown", "dirty": true, "package": "docker", "debugBuild": false, "buildEnvironment": {"-buildmode":"exe","CGO_ENABLED":"0","GOAMD64":"v1","GOARCH":"amd64","GOOS":"linux","buildtags":"ferretdb_tigris","compiler":"gc","vcs":"git","vcs.time":"2023-04-03T17:19:58Z"}, "uuid": "299725a4-0eed-4013-a7cb-d3c4a8c84662"}
ferretdb-ferretdb-1  | 2023-05-11T09:12:36.834+0900 INFO  telemetry telemetry/reporter.go:145 The telemetry state is undecided; the first report will be sent in 1h0m0s. Read more about FerretDB telemetry and how to opt out at https://beacon.ferretdb.io.
ferretdb-ferretdb-1  | 2023-05-11T09:12:36.835+0900 INFO  listener  clientconn/listener.go:95 Listening on TCP [::]:27017 ...
ferretdb-ferretdb-1  | 2023-05-11T09:12:36.836+0900 INFO  listener  clientconn/listener.go:183  Waiting for all connections to stop...
ferretdb-ferretdb-1  | 2023-05-11T09:12:36.836+0900 INFO  debug debug/debug.go:95 Starting debug server on http://[::]:8080/
ferretdb-ferretdb-1  | 2023-05-11T09:12:45.859+0900 INFO  listener  clientconn/listener.go:305  Connection started  {"conn": "172.20.0.2:53430 -> 172.20.0.4:27017"}
ferretdb-ferretdb-1  | 2023-05-11T09:12:45.957+0900 INFO  listener  clientconn/listener.go:305  Connection started  {"conn": "172.20.0.2:53442 -> 172.20.0.4:27017"}

起動時のエラーも出なくなり、正常に構成されたことがわかります!

仕組みについておさらい

依存される側

healthcheckオプションを追加し、自身のヘルスチェックを行うコマンドを記述します。
今回のケースでは postgresql の機能であるpg_isreadyコマンドが正常に終了した状態をhealtyとするようにしました。intervalretriesはよしなに指定します。

依存する側

depends_onconditionオプションを追加し、service_healthyを指定すると、コンテナは依存先コンテナのヘルスチェックが完了するまで、Waiting状態で待機するようになります。

まとめ

以上です。

Docker Compose で複数のコンテナを構成するにあたり、あるコンテナが別のコンテナに強く依存している場合の起動順のコントロール方法についてご紹介しました。

依存されるコンテナやサービスによってヘルスチェックの方法は様々あると思いますので、ぜひご参考いただいて活用いただければ幸いです。

ではまた!

参考記事

脚注
  1. 実はタイミング次第でうまくいってしまう場合もあります。 ↩︎

Discussion