docker composeで依存するサービスが起動するまで待機する

## はじめに

以下のような`docker-compose.yaml`があった時に、`app`（アプリ）の起動時にはそのアプリが依存するMySQLやRedisが立ち上がった状態になっていてほしいことはままある。

```yaml:docker-compose.yaml
version: '3.9'
services:
  app:
    container_name: app
    image: app:latest
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - 8080:3000
  redis:
    image: redis:7.2.1-alpine3.18
    container_name: redis
    environment:
      TZ: 'Asia/Tokyo'
    volumes:
      - ./data/redis:/data
    ports:
      - 6379:6379
  mysql:
    image: mysql:8.0.32
    container_name: mysql
    environment:
      MYSQL_ROOT_PASSWORD: ''
      MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
      TZ: 'Asia/Tokyo'
    ports:
      - 3306:3306
    volumes:
      - ./data/mysql:/var/lib/mysql
      - ./mysql/sql:/docker-entrypoint-initdb.d
      - ./mysql/my.cnf:/etc/mysql/conf.d/my.cnf
```

そのような場合に、どうしたら依存サービスの起動（正確にはスタンバイ）を待ってアプリのコンテナを起動するようにできるか？について、今回は見ていきたいと思う。

## healthcheckとdepends_onを利用する

結論としては、以下のように`healthcheck`と`depends_on`を利用すればいい。

```yaml:
version: '3.9'
services:
  app:
    ...
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    ports:
      - 8080:3000
  redis:
    ...
    healthcheck:
      test: ['CMD', 'redis-cli', 'ping']
      interval: 10s
      timeout: 5s
      retries: 3
    ...
  mysql:
    ...
    healthcheck:
      test: ['CMD', 'mysqladmin', 'ping', '-h', 'localhost']
      interval: 10s
      timeout: 5s
      retries: 3
    ...
```

以下で詳細を見ていく。

### [healthcheck](https://docs.docker.com/compose/compose-file/05-services/#healthcheck)

> healthcheck declares a check that's run to determine whether or not the service containers are "healthy".

と書かれている通り、healthcheckによりコンテナが正常に起動していることを確認できる。そのコマンドは`test`のキーに定義することができ、このコマンドをcomposeが実行してくれる（ちなみに、Dockerfile内の[HEALTHCHECK](https://docs.docker.com/engine/reference/builder/#healthcheck)はcomposeで定義されているものがあれば上書きされる）。

具体的に今回の例で見ていくと、MySQL・Redisでそれぞれ以下のようなことをしている。

- MySQL
  - `mysqladmin ping -h localhost`：mysqladminコマンドでMySQLサーバーにpingを送り、その応答を確認する（詳細は[4.5.2 mysqladmin — A MySQL Server Administration Program](https://dev.mysql.com/doc/refman/8.0/en/mysqladmin.html)を参照）
- Redis
  - `redis-cli ping`：redis-cliコマンドでRedisサーバーにpingを送り、その応答を確認する（詳細は[Redis CLI](https://redis.io/docs/connect/cli/)を参照）

つまりは、それぞれのサーバーでpingをして応答を確かめている。

そして、`interval`, `timeout`, `retries`はそれぞれ以下のことを設定している。

- interval：testに設定されたコマンドをどれくらいのサイクルで実行するか？を設定する。コンテナが起動してからインターバル秒後に最初に実行され、前回のチェックが完了するたびにインターバル秒後に再度実行される。
- timeout：testに設定されたコマンドを実行した際に、そのコマンドの実行にどれだけ時間がかかったら失敗とみなすか？を設定する。タイムアウト秒数以上かかった場合、チェックは失敗したとみなされる。
- retries：コンテナが`unhealthy`と判定されたときに、何回testに設定されたコマンドを実行するか？を設定する。

### [depends_on](https://docs.docker.com/compose/compose-file/05-services/#depends_on)

続いて`depends_on`についてだが、以下のように記載がある通り、コンテナサービス間のスタート・シャットダウンの順番の制御に関わる設定になる。

> depends_on expresses startup and shutdown dependencies between services.

`condition`に`service_healthy`を指定することで、依存サービスを開始する前に、依存関係（今回でいうとMySQL・Redisのそれぞれ）がhealthcheckの設定で定義される条件を満たし、healthyであるか？をチェックし、healthyの場合にコンテナを起動するという設定をすることができる。

- 参考：https://docs.docker.com/compose/compose-file/05-services/#depends_on

## まとめとして

上記のそれぞれの設定を組み合わせることで、MySQL・Redisのそれぞれがhealthcheckの結果、healthyとみなされる＝問題なく起動しスタンバイになっている場合に、アプリのコンテナを起動させることができるようになる。