Webアプリ(React + Flask + MySQL)にDocker(Compose)を導入して詰まった所をまとめてみた

# はじめに
Webアプリ(React + Flask + MySQL)にDocker(Compose)を導入した際に色々と詰まったのでまとめてみました。なおローカルの開発環境での実行を前提としています。

# フォルダ構成
```
.
├─ app
│  └─ backend(Flaskコンテナ)
│    └─ requirement.txt
│    └─ ...
│  └─ frontend(Reactコンテナ)
│    └─ package.json
│    └─ yarn.lock 
│    └─ ...
├─ db
│  └─ mysql(MySQLコンテナ)
│    └─ ...
└─ docker
   └─ backend
     └─ Dockerfile
   └─ frontend
     └─ Dockerfile
   └─ mysql
     └─ Dockerfile
   └─ compose.yml
```

# アーキテクチャ
ip: 0.0.0.0がポイント

![241016_docker.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3088985/7bb1ed65-49c5-c417-3a1f-a28a5139c56c.png)

# 詰まった所
## COPYができない
以下のように指定するとCOPYができない。
```:Dockerfile
WORKDIR usr/src/app/backend
COPY ../app/backend .
```
```compose.yml
build:
    context: .
    dockerfile: ./backend/Dockerfile
```
### 解決策
以下のように指定するとCOPYができる。
```compose.yml
build:
    context: ../
    dockerfile: docker/backend/Dockerfile
```

### 原因
Dockerは基本的にDockerfileがあるディレクトリ以下にあるファイルしかビルドすることができない。ここで、compose.ymlでbuild-contextを上の階層に調整し、dockerfileは「contextで指定したディレクトリ」から見たパスを指定する。
これで、ビルド対象のファイルをDockerfileがあるディレクトリ以下にあるファイルとみなすことができる。

### 補足 
- build-contextとはbuild時に指定するフォルダのこと
- build-contextにあるファイルに対してCOPY等の操作を行うことができる

## COPYとバインドマウントの関係について
コンテナにスクリプトやデータをコピーすると重くなるので、スクリプトやデータはホストに置き、コンテナには実行環境のみコピーして、バインドマウントによりスクリプトを実行する。
つまり、requirement.txtのみCOPYし実行環境を構築する。
なお、compose.ymlのvolumesでは相対パスで記述可能となる。
```:Dockerfile
WORKDIR usr/src/app/backend
COPY app/backend/requirement.txt .
RUN pip install -r requirement.txt
```
```compose.yml
volumes:
    - ../app/backend:.
```

また、gitでnode_modulesをコミット対象外とする場合は、新たに環境構築する際、ホスト側にnode_modulesがない状態でコンテナを作成することになる。この時、Reactコンテナの方は「../app/frontend:.」とバインドマウントするとコンテナのnode_modulesを上書きしエラーとなる。
よって、「usr/src/app/frontend/node_modules」によりコンテナにnode_modulesを作成する必要がある。
```:Dockerfile
WORKDIR usr/src/app/frontend
COPY app/frontend/package.json app/frontend/yarn.lock .
RUN yarn install
```
```compose.yml
volumes:
    - ../app/frontend:.
    - usr/src/app/frontend/node_modules
```

### 補足 
- バインドマウントとはホストのファイルをコンテナのファイルのように扱うことができること

## MySQLにアクセスができない
以下のように指定したがアクセスができなかった。
```config.yml
mysql:
    host=localhost
    port=3306
```
```compose.yml
mysql:
    ports: 3306:3306
```
### 解決策
以下のように指定した所アクセスができた。
```config.yml
mysql:
    host=mysql
    port=3306
```

### 原因
ここでのlocalhostはホストではなくFlaskコンテナ自身を指すためMySQLコンテナへアクセスはできない。ポートフォワードはホストからコンテナへのアクセスとなり、コンテナからコンテナへのアクセスは不可となる。
よって、コンテナ間通信はコンテナ名を使用する必要がある。

## React・Flaskにローカルブラウザからアクセスができない①
以下のように指定したがアクセスができなかった。
```package.json
{
  "scripts": {
    "HOSTの指定なし(デフォルトはlocalhost)"
  }
}
```
```run.py
if __name__ == '__main__':
    # HOSTの指定なし(デフォルトはlocalhost)
    app.run(port=5000)
```
```compose.yml
frontend:
    ports: 3000:3000

backend:
    ports: 5000:5000
```
### 解決策
以下のように指定した所アクセスができた。
```package.json
{
  "scripts": {
    "start": "HOST=0.0.0.0 react-scripts start"
  }
}
```
```run.py
if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
```

### 原因
localhostは内部(自分)からのアクセスのみ可能で外部からのアクセスは不可能となる。ここで、0.0.0.0を開発サーバとして立てた場合に限り、同一ネットワーク上にある全てのデバイスからアクセス可能なサーバーになる。つまり、外部からのアクセスが可能となる。
よって、コンテナ内のReact・Flaskサーバーを0.0.0.0として立てれば、ホスト(外部)からのアクセスが可能となる。

## React・Flaskにローカルブラウザからアクセスができない②
以下のように指定したがアクセスができなかった。
```compose.yml
backend:
    # 特段指定なし
frontend:
    depends_on:
        backend
```
### 解決策
以下のように指定した所アクセスができた。
```compose.yml
backend:
    health_check:
        test: ["CMD", "curl", "-f", "http://localhost:5000"]
        interval: 30s
        timeout: 30s
        retires: 3

frontend:
    depends_on:
        backend:
            condition: service_healthy
```

### 原因
コンテナ間の依存関係を保つために、backendのコンテナ作成後にfrontendのコンテナを作成するようdepends_onで指定していたがアクセスできないことがあった。これは、depends_onがコンテナの起動順は制御するが依存先のコンテナ(backend)で必要なサービスが起動しているかまではチェックしないためである。
そこで、health_checkを利用し、depends_onのconditionで依存先のコンテナ(backend)で必要なサービスが起動しているかまでチェックしてからfrontendのコンテナを作成する設定をすることで、コンテナ間の依存関係を保つことができる。

## print文がターミナルに出力されない
以下のように指定したが出力ができなかった。
```:Dockerfile
CMD ["python", "app.py"]
```
### 解決策
以下のようにログにより出力ができた。
```:ターミナル
docker compose logs backend -f
### コンテナ内
ここにprint文が出力される
```
```:Dockerfile
CMD ["python", "-u", "app.py"]
```

### 補足
- -uオプションはPythonをunbufferedモードで実行し、標準出力と標準エラーを即ターミナルに表示させることができる

# おわりに
Dockerを理解するにはNetworkとLinuxの知識が必須のためWebアプリ作成のためにはインフラの知識が重要と改めて認識しました。

# 参考文献
- [ChatGPT](https://chatgpt.com/)
- [米国AI開発者がゼロから教えるDocker講座](https://www.udemy.com/course/aidocker/)
- [コンテナビルド時にDockerファイルより上のディレクトリを用いる方法](https://seiseiengineering.com/%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E3%83%93%E3%83%AB%E3%83%89%E6%99%82%E3%81%ABdocker%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%82%88%E3%82%8A%E4%B8%8A%E3%81%AE%E3%83%87%E3%82%A3%E3%83%AC%E3%82%AF/)
- [【Docker】コンテナ内のnode_modulesが消える問題を改善する](https://qiita.com/P-man_Brown/items/c2a69d943853cb381fbe)
- [Dockerコンテナ上で立てた開発サーバに別端末でアクセスする方法](https://qiita.com/progra_dango/items/a115bc038c45879df83f)
- [Docker の healthcheck を初めて使った話](https://zenn.dev/sun_asterisk/articles/b4b17681d08018)
- [Docker Compose で依存先のコンテナが「ちゃんと」起動しているかチェックするひとつの方法](https://zenn.dev/daifukuninja/articles/35c8c9812391f7)
- [Docker Compose の depends_on の使い方まとめ](https://gotohayato.com/content/533/)