【Docker番外編①】DjangoのコンテナからMySQLのコンテナにアクセスできない？

## はじめに
こんにちは！ITスクールRareTECHにてCS（Customer Support）を担当している池村です。今回の記事はDocker Composeでのコンテナ起動順のエラー解決についてです。先日うちの受講生からコンテナ起動がうまくいかないと相談があったので、その解決までの流れを残しておきます。


:::note warn
この記事はDocker Composeまでを学習した人向けです。
:::

![3ca86cea-c925-4947-963e-d8e75bc87c21_720.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3798971/65636380-fb60-6015-8dfe-915ea5f1f2a3.png)


## エラー内容
まず今回の環境構築の内容は以下です。
- Djangoのコンテナ
- MySQLのコンテナ
- Nginxのコンテナ

上記のコンテナをComposeで起動して、Djangoアプリケーションを動かすのが目的になっています。

ディレクトリ構成は以下になります。
```Bash:ディレクトリ構成
.
├── fapp
│   ├── __pycache__
│   ├── migrations
│   │   └── __pycache__
│   └── templates
├── fproject
│   └── __pycache__
├── infra
│   ├── app
│   ├── db
│   │   └── db_data
│   │       ├── #innodb_redo
│   │       ├── #innodb_temp
│   │       ├── fteam_db
│   │       ├── mysql
│   │       ├── performance_schema
│   │       └── sys
│   └── nginx
└── static
    └── admin
        ├── css
        │   └── vendor
        │       └── select2
        ├── img
        │   └── gis
        └── js
            ├── admin
            └── vendor
                ├── jquery
                ├── select2
                │   └── i18n
                └── xregexp

```

※ファイル自体は非表示

docker-compose.ymlファイルは以下です。
```YAML:docker-compose.ymlの中身
services:
  app:
    build:
      context: .
      dockerfile: ./infra/app/Dockerfile
    working_dir: "/usr/src/app"
    tty: true
    command: python manage.py runserver 0.0.0.0:8000 
    volumes:
      - ./:/usr/src/app
    ports:
      - "8080:8000"
    depends_on:
      db: 
        condition: service_healthy

  db:
    build:
      context: .
      dockerfile: ./infra/db/Dockerfile
    tty: true
    volumes:
      - ./infra/db/db_data:/var/lib/mysql
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}

  nginx:
    image: nginx:latest
    ports:
      - "80:80" 
    volumes:
      - ./infra/nginx/fnginx.conf:/etc/nginx/conf.d/default.conf # NginX設定ファイルのマウント
      - ./static:/usr/src/app/static 
    depends_on:
      - app 

```

いざ`docker compose up`を行ったものの、以下のエラーが出てしまっていました。
```Bash:エラー内容
app-1    |   File "/usr/local/lib/python3.11/site-packages/django/utils/asyncio.py", line 26, in inner
app-1    |     return func(*args, **kwargs)
app-1    |            ^^^^^^^^^^^^^^^^^^^^^
app-1    |   File "/usr/local/lib/python3.11/site-packages/django/db/backends/mysql/base.py", line 256, in get_new_connection
app-1    |     connection = Database.connect(**conn_params)
app-1    |                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
app-1    |   File "/usr/local/lib/python3.11/site-packages/MySQLdb/__init__.py", line 121, in Connect
app-1    |     return Connection(*args, **kwargs)
app-1    |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^
app-1    |   File "/usr/local/lib/python3.11/site-packages/MySQLdb/connections.py", line 200, in __init__
app-1    |     super().__init__(*args, **kwargs2)
app-1    | django.db.utils.OperationalError: (2002, "Can't connect to server on 'db' (115)")
db-1     | 2025-01-19T04:12:30.782723Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended.
db-1     | 2025-01-19T04:12:30.906233Z 0 [System] [MY-010229] [Server] Starting XA crash recovery...
db-1     | 2025-01-19T04:12:30.914796Z 0 [System] [MY-010232] [Server] XA crash recovery finished.
db-1     | 2025-01-19T04:12:30.986170Z 0 [Warning] [MY-010068] [Server] CA certificate ca.pem is self signed.
db-1     | 2025-01-19T04:12:30.986202Z 0 [System] [MY-013602] [Server] Channel mysql_main configured to support TLS. Encrypted connections are now supported for this channel.
db-1     | 2025-01-19T04:12:30.991049Z 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory.
db-1     | 2025-01-19T04:12:31.011815Z 0 [System] [MY-011323] [Server] X Plugin ready for connections. Bind-address: '::' port: 33060, socket: /var/run/mysqld/mysqlx.sock
db-1     | 2025-01-19T04:12:31.011830Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.40'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server - GPL.
```



## ファイルにおかしい記述がないか？
最初に目に入ったのは、`django.db.utils.OperationalError: (2002, "Can't connect to server on 'db' (115)")`の部分でした。

DjangoからMySQLにコネクションできていないのは明らかだったので、設定ファイル類を確認しに行ってみました。
- yamlファイルの記述
- settings.pyの記述
- .envファイルの記述

これらどれも見に行ったんですが、特に問題はありませんでした。
ネットワークの問題か？となったので次に移ります。

## コンテナ同士のネットワークは問題ないか？
コンテナ同士のネットワークが不完全ならエラーも吐くだろうと思い、まずはネットワークの確認を実施。

```Bash:ネットワークの確認コマンド
docker network ls
docker network inspect 2025winter-f_default
```
```Bash:表示結果
[
    {
        "Name": "2025winter-f_default",
        "Id": "11689ce9407c6fb7a24dd2bf1855489e9ce2c7ba1fc313f5b153ca4605073cc7",
        "Created": "2025-01-19T13:12:29.544150333+09:00",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "192.168.155.0/24",
                    "Gateway": "192.168.155.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "3fff70454185ea4052c60eceae22cc27291bad0ab444ce8fe772611d96d81734": {
                "Name": "2025winter-f-app-1",
                "EndpointID": "85d2b8e85874a6d7e040b8f17e45a7ef563db122b1415174f14c1fdc20d106be",
                "MacAddress": "02:42:c0:a8:9b:03",
                "IPv4Address": "192.168.155.3/24",
                "IPv6Address": ""
            },
            "40f6838d5badbf7c7adb1c7913792635d5c991992a5a1b228df5cb415d9e20eb": {
                "Name": "2025winter-f-db-1",
                "EndpointID": "3505bfd7cc7d9b7a195cbc8a044449279ab471915d1d468ba88cd7bb31451691",
                "MacAddress": "02:42:c0:a8:9b:02",
                "IPv4Address": "192.168.155.2/24",
                "IPv6Address": ""
            },
            "661c69aba669b6c7582401122336267a8b6c7418139ffe5cba661623c1ee89bc": {
                "Name": "2025winter-f-nginx-1",
                "EndpointID": "a8768c4fef514fee306ea5abaf90f75f34b3745e4dd0983bb95ea0524c6cbe2f",
                "MacAddress": "02:42:c0:a8:9b:04",
                "IPv4Address": "192.168.155.4/24",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {
            "com.docker.compose.config-hash": "a9da0cb733a47ac917cae33f2192776cb3391d52500c4c004ddee21e33aea9ad",
            "com.docker.compose.network": "default",
            "com.docker.compose.project": "2025winter-f",
            "com.docker.compose.version": "83.0.0"
        }
    }
]

```

:::note info
特に問題なし。コンテナ間は同じネットワークにいる。
:::



## コンテナの中に入って確認
まあこれだけだと本当に繋がっているのか確信はなかったので、今度はコンテナの中に入ってコンテナ同士がつながるかどうかを確認してみます。あとやはりしっかりデータベースの中身も確認していきます。

#### MySQLのコンテナの中でデータベースができているか確認
まずはMySQLの中に入って、Djangoからアクセスする際のユーザー情報は合っているのか？と、実際にテーブルを見に行ってみます。

```Bash:コンテナの中に入る
docker exec -it コンテナ名 bash
```

```Bash:MySQLの中に入る
mysql -u root -p
```
※rootパスワードを入力して入ります。

```Bash:ユーザーの権限まわり
SHOW GRANTS FOR 'ユーザー名'@'%';
+-----------------------------------------------------------+
| Grants for ユーザー名@%                                     |
+-----------------------------------------------------------+
| GRANT USAGE ON *.* TO `ユーザー名`@`%`                      |
| GRANT ALL PRIVILEGES ON `データベース名`.* TO `ユーザー名`@`%` |
+-----------------------------------------------------------+

```

:::note info
結論、データベースもテーブルもしっかりできており、今回使用するMySQLのユーザーにも適切な権限が付与されていました。
:::


#### Djangoのコンテナの中から疎通確認 + DBアクセス
じゃあ次はと、今度はDjangoのコンテナの中に入ってデータベースにつながるかどうかを試してみます。

```Bash:コンテナの中に入る
docker exec -it コンテナ名 bash
```

最初は何も入っていないので、まずは`ping`を入れてみます。
```Bash:インストール方法
apt update
apt install iputils-ping -y
```
※コンテナの中にはrootで入るので、`sudo`はいりません。

```Bash:疎通確認
ping db
```

```Bash:実行結果
root@3fff70454185:/usr/src/app# ping db
PING db (192.168.155.2) 56(84) bytes of data.
64 bytes from 2025winter-f-db-1.2025winter-f_default (192.168.155.2): icmp_seq=1 ttl=64 time=1.66 ms
64 bytes from 2025winter-f-db-1.2025winter-f_default (192.168.155.2): icmp_seq=2 ttl=64 time=0.309 ms
64 bytes from 2025winter-f-db-1.2025winter-f_default (192.168.155.2): icmp_seq=3 ttl=64 time=0.082 ms
```
:::note info
こちらも`ping`が通る時点で繋がっているようです。
:::

ではMySQLにDjangoから接続できないのか？

```Bash:MySQLをコンテナに入れて接続してみる
apt install mysql-client -y
```
もしエラーを吐くなら以下でも大丈夫です。
```Bash:MariaDBでも可能
apt install mariadb-client -y
```

では接続してみます。
```Bash:書き方
mysql -u root -p
```

:::note info
こちらも無事接続完了。じゃあどこだ！
:::

## 改めてDockerのログを見てみる
Composeでコンテナを起動すると、そのログが表示されるのですが、そこにしっかりヒントがありました。

```Bash:書き方
InnoDB initialization has ended.
```

初期化が完了しました。がDjangoのコンテナのエラーの後に表示されていたんですね。

:::note alert
ということは、コンテナの起動順じゃないか？という仮説になります。
:::

```Bash:試してみる
docker stop Djangoのコンテナ
docker start Djangoのコンテナ
```

これをすると、問題なくコンテナが起動しました。

## 結論
これは結局、MySQLのコンテナ作成が遅かったことによるエラーでした。
いくらdocker-compose.ymlファイルに` depends_on:`の記述をしていても、あくまでコンテナ作成順を保証しているのであって、コンテナの中で実行されるコマンド類はまた別問題なんですね。

## 解決方法
結局コンテナの作成順の問題なら、ヘルスチェックすれば解決します。
簡単にヘルスチェックについて解説します。

`healthcheck`は、コンテナ内のサービスが正常に動作しているかを監視するためのDockerの機能になります。

```YAML:書き方
db:
    build:
      context: .
      dockerfile: ./infra/db/Dockerfile
    tty: true
    volumes:
      - ./infra/db/db_data:/var/lib/mysql
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}
      MYSQL_USER: ${MYSQL_USER}
      MYSQL_PASSWORD: ${MYSQL_PASSWORD}
    healthcheck:
      test:
        [
          "CMD-SHELL",
          "mysqladmin ping -h localhost -u ${MYSQL_USER} -p${MYSQL_PASSWORD} || exit 1",
        ]
      interval: 10s
      timeout: 5s
      retries: 5
```
上記のように`healthcheck:`を記述して、pingでMySQLにアクセスできるかどうかを見ています。
- interval：テストを実行する間隔を指定
- timeout：テストコマンドがタイムアウトするまでの時間を指定
- retries：テストが失敗した場合に再試行する回数を指定

:::note info
コマンド内容は、MySQLサーバーに対してpingを実行し、成功すれば0（正常）を返し、失敗すれば1（異常）を返します。
:::


```YAML:Djangoの方にも
depends_on:
  db: 
    condition: service_healthy
```

依存先のサービス（dbのこと）が完全に起動して、健康状態が「healthy」と判定されるまで待機してくれます。

:::note info
コマンドの実行結果が0の際にはhealthyと判定されるということですね。
:::



## おわりに
やはりログをみるのは大事ですね！英語といってもしっかり確認しないと無駄な時間がかかってしまってもったいないです。

ITスクールRareTECHでは年に4回チーム開発イベント、ハッカソンを行っています。受講生の方のレベルもどんどん上がっていて期待してしまいます。

今回はDockerの番外編ということで、誰かのエラー解決のきっかけになることを祈っています。