【Docker】複数コンテナにおける接続エラーを解決する

# 概要
DBコンテナ（MySQL）とApplicationコンテナ（SpringBoot）の2つをdocker-compose.ymlにて管理しようとしました。ビルド実行すると、Applicationコンテナのビルドに失敗したため、以下のエラーについての解決方法を調査検討しました。

## エラー内容
```text
Caused by: java.net.UnknownHostException: db: Name or service not known
```
databaseのmigrationにFlywayを使っていたので、以下のようなエラーも出ていました。
```text
Unable to obtain connection from database: Communications link failure
Error creating bean with name ‘flywayInitializer’ defined in class path resource
```

# 最終結果
いろいろ試したのですが、先に最終的に設定したフォルダ構成とコンテナ設定ファイルを載せます。

<details><summary>ソースコード</summary>

```text:フォルダ構造（一部省略）
├── docker-compose.yml
├── mysql（DBコンテナ）
│   ├── Dockerfile
│   ├── data
│   └── settings
└── spring（Appコンテナ）
    ├── Dockerfile
    └── demo
```

```Dockerfile:mysql/Dockerfile
FROM mysql:8.0.28-debian
EXPOSE 3306
```

```Dockerfile:spring/Dockerfile
FROM openjdk:11
WORKDIR /app
EXPOSE 8080
COPY ./demo /app/
CMD ["sh", "-c", "mvn clean package && java -jar /app/target/demo-0.0.1-SNAPSHOT.jar"]
```

```yaml:docker-compose.yml
version: '3.9'
services:
  db:
    container_name: db
    build:
      context: ./mysql
    ports:
      - 3306:3306
    command: --default-authentication-plugin=mysql_native_password
    volumes:
      - ./mysql/settings:/etc/mysql/conf.d/
      - ./mysql/data/:/var/lib/mysql/
    env_file:
      - .env
    healthcheck:
      test: ["CMD", "mysqladmin" ,"ping", "-h", "${MYSQL_HOST}", "-u${MYSQL_USER}", "-p${MYSQL_PASSWORD}"]
      timeout: 20s
      retries: 10
    networks:
      - app-net
  app:
    build:
      context: ./spring
    ports:
      - 8080:8080
    restart: always
    depends_on:
      db:
        condition: service_healthy
    networks:
      - app-net
networks:
  app-net:
    driver: bridge
```

```.env
MYSQL_DATABASE=<DB名>
MYSQL_USER=<DBユーザー名>
MYSQL_PASSWORD=<DBパスワード>
MYSQL_HOST=<DBホスト名>
```

</details>

# エラーの原因分析と切り分け
ここからは、上記エラーとして考えられる原因、原因切り分けのためにやるべきこと、原因に対する解決方法をまとめます。エラーの原因として以下の3パターンが考えられます。
1. dbコンテナが立ち上がる前にアクセスしている
2. dbコンテナとappコンテナが同一のネットワークに存在していない
3. アプリケーション側のDB設定が正しくない

以下では、それぞれの詳細を述べます。

## 1. dbコンテナが立ち上がる前にアクセスしている

### 切り分け
appコンテナが起動できるならば、execで中に入って`mvn clean package`をしてみましょう。正常にビルドされたらコンテナの実行タイミングが問題です。私はビルド時にテスト実行されてDBアクセス不良でエラーになっていたため、テストをスキップすることでコンテナ起動しました。
```diff:spring/Dockerfile
- RUN mvn clean package
+ RUN mvn -B -DskipTests clean package
```
### 解決法
力技で解決してしまった感があるため、正しい解決法があればご教示ください。以下の1-1, 1-2の実施が必要です。

#### 1-1. docker-compose.ymlのdepends_onプロパティを利用する
docker-compose.ymlに`depends_on`プロパティを設定することで、dbコンテナがreadyになったらappコンテナを実行するように設定します。

`service_healthy`を設定することで、db serviceのヘルスチェックが成功してからapp serviceを起動させることができます。

https://docs.docker.com/compose/startup-order/

```diff_yaml:docker-compose.yml（抜粋）
version: '3.9'
services:
  db:
+    healthcheck:
+      test: ["CMD", "mysqladmin" ,"ping", "-h", "${MYSQL_HOST}", "-u${MYSQL_USER}", "-p${MYSQL_PASSWORD}"]
+      timeout: 20s
+      retries: 10
  app:
+    depends_on:
+      db:
+        condition: service_healthy
```

:::note warn
注意1
docker-composeのversion3以降ではdepends_on の条件形式はサポートされなくなりました。しかし、version3.9以降で復活しています。
適切なversionを設定してください。
https://github.com/docker/compose/issues/8154
:::

:::note warn
注意2
Docker Composeのバージョンが古いとversion3.9が利用できない可能性があります。aptからのインストールはバージョンが古かったので、公式レポジトリからv.1.29.2を落としてインストールしました。（Compose v2はdocker composeコマンドになるため、[LocalStack](https://docs.localstack.cloud/getting-started/installation/#docker-compose)が使えず上記バージョンにしました。問題ない方は最新バージョンでもよいと思います）
https://github.com/docker/compose/releases
:::

.envファイルにDB接続情報を記載してください。DBホスト名はdocker-compose.ymlのservice名（今回であればdb）です。

#### 1.2. CMDでビルド実行する
上記対応でコンテナの起動順は制御できましたが、Dockerイメージのビルドプロセス（RUNコマンドなど）は変わりません。つまり、RUNで定義している`mvn clean package`はMySQLコンテナが起動していなくても実行されてしまうのです。したがって、このコマンドをCMDで実行します。

```diff:Dockerfile
- RUN mvn clean package
- ENTRYPOINT ["java","-jar","/app/target/demo-0.0.1-SNAPSHOT.jar"]
+ CMD ["sh", "-c", "mvn clean package && java -jar /app/target/demo-0.0.1-SNAPSHOT.jar"]
```

##### （補足）CMDとRUNコマンドの違い
RUN：DockerfileからImageを作成するときに一度だけ実行されます
CMD：Docker ImageからDockerコンテナを作成するときに実行されます

## 2. dbコンテナとappコンテナが同一のネットワークに存在していない
### 切り分け
appコンテナが起動できるならば、execで中に入ってpingを飛ばします。
今回は失敗したところの直前までビルドして、execして以下を実行しました。
※ dbはdocker-compose.ymlのservice名です
```terminal
apt-get update && apt-get install iputils-ping
ping db
```
レスポンスが返ってきていなければ、network設定を見直しましょう。

### 解決法
2つのcontainerを同一networkに入れましょう。
参考：https://docs.docker.jp/compose/networking.html
```diff_yaml:docker-compose.yml（抜粋）
  services:
    db:
+     networks:
+       - app-net

    app:
+     networks:
+       - app-net
+ networks:
+   app-net:
+     driver: bridge
```

## 3. アプリケーション側のDB設定が正しくない
SpringBootであれば、application.propertiesの`datasource.url`, `datasource.username`, `datasource.password`が正しく設定されていない可能性があります。

### 切り分け
Dockerの問題なのかを確かめます。ローカルにMySQLやSpringBoot環境を作って実行してみて、正しく動かなければこの可能性が高いです。ただ、そこまでする前にもう一度設定を見直すのが良いと思います。

### 解決法
正しく設定しましょう。
参考：https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html
```properties:application.properties
spring.datasource.url=jdbc:mysql://<DBホスト名>:3306/<DB名>
spring.datasource.username=<DBユーザー名>
spring.datasource.password=<DBパスワード>
```