はじめに
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がポイント
詰まった所
COPYができない
以下のように指定するとCOPYができない。
WORKDIR usr/src/app/backend
COPY ../app/backend .
build:
context: .
dockerfile: ./backend/Dockerfile
解決策
以下のように指定するとCOPYができる。
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では相対パスで記述可能となる。
WORKDIR usr/src/app/backend
COPY app/backend/requirement.txt .
RUN pip install -r requirement.txt
volumes:
- ../app/backend:.
また、gitでnode_modulesをコミット対象外とする場合は、新たに環境構築する際、ホスト側にnode_modulesがない状態でコンテナを作成することになる。この時、Reactコンテナの方は「../app/frontend:.」とバインドマウントするとコンテナのnode_modulesを上書きしエラーとなる。
よって、「usr/src/app/frontend/node_modules」によりコンテナにnode_modulesを作成する必要がある。
WORKDIR usr/src/app/frontend
COPY app/frontend/package.json app/frontend/yarn.lock .
RUN yarn install
volumes:
- ../app/frontend:.
- usr/src/app/frontend/node_modules
補足
- バインドマウントとはホストのファイルをコンテナのファイルのように扱うことができること
MySQLにアクセスができない
以下のように指定したがアクセスができなかった。
mysql:
host=localhost
port=3306
mysql:
ports: 3306:3306
解決策
以下のように指定した所アクセスができた。
mysql:
host=mysql
port=3306
原因
ここでのlocalhostはホストではなくFlaskコンテナ自身を指すためMySQLコンテナへアクセスはできない。ポートフォワードはホストからコンテナへのアクセスとなり、コンテナからコンテナへのアクセスは不可となる。
よって、コンテナ間通信はコンテナ名を使用する必要がある。
React・Flaskにローカルブラウザからアクセスができない①
以下のように指定したがアクセスができなかった。
{
"scripts": {
"HOSTの指定なし(デフォルトはlocalhost)"
}
}
if __name__ == '__main__':
# HOSTの指定なし(デフォルトはlocalhost)
app.run(port=5000)
frontend:
ports: 3000:3000
backend:
ports: 5000:5000
解決策
以下のように指定した所アクセスができた。
{
"scripts": {
"start": "HOST=0.0.0.0 react-scripts start"
}
}
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にローカルブラウザからアクセスができない②
以下のように指定したがアクセスができなかった。
backend:
# 特段指定なし
frontend:
depends_on:
backend
解決策
以下のように指定した所アクセスができた。
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文がターミナルに出力されない
以下のように指定したが出力ができなかった。
CMD ["python", "app.py"]
解決策
以下のようにログにより出力ができた。
docker compose logs backend -f
### コンテナ内
ここにprint文が出力される
CMD ["python", "-u", "app.py"]
補足
- -uオプションはPythonをunbufferedモードで実行し、標準出力と標準エラーを即ターミナルに表示させることができる
おわりに
Dockerを理解するにはNetworkとLinuxの知識が必須のためWebアプリ作成のためにはインフラの知識が重要と改めて認識しました。