Docker Composeで３層アーキテクチャ（ JS + Python + MySQL）を構築する

# はじめに

Docker Composeで３層アーキテクチャの開発環境を構築していきます。

* プレゼンテーション層はJavaScript
* アプリケーション層はPython
* データ層はMySQL

を使用します。

ソースコードは[こちら](https://github.com/ayakakawabe/docker-three-tiered-architecture)

![スクリーンショット 2024-04-08 13.22.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2784580/e6f6a7b3-a4a5-04f5-1cfd-b9513fc92e3c.png)

## 開発環境

* MacBook M2
* Docker v.25.0.3
* Docker Compose v2.24.5

## ディレクトリ構成

```
root/
    ├ web/
    |   └ sample.js
    ├ app/
    |   ├ api.py
    |   └ con_db.py
    ├ docker/
    |   ├ web_context/
    |   |   └ Dockerfile
    |   ├ app_context/
    |   |   ├ Dockerfile
    |   |   └ requirements.txt
    |   ├ db_context/
    |       ├ Dockerfile
    |       └ init/
    |           └ init.sql
    └ docker-compose.yml
```

# 手順

## 1. Webコンテナイメージの作成
`./docker/web_context/`に`Dockerfile`を作成します。

```dockerfile:Dockerfile
# 元イメージを定義
FROM node:20.12-bullseye-slim

# apt-getの更新
RUN apt-get update && apt-get -y install
```


 ## 2. Appコンテナイメージの作成
 
 `./docker/app_context/`に`requirements.txt`を作成します。
 
今回は、APIをFastAPIで作成するので、`fastapi`と`uvicorn`を追加します。
また、MySQLと通信するために`mysql-connector-python`も追加します。

```:requirements.txt
fastapi==0.110.1
uvicorn==0.29.0
mysql-connector-python==8.3.0
```


`./docker/app_context/`に`Dockerfile`を作成します。

```dockerfile:Dockerfile
# 元イメージを定義
FROM python:3.11.9-slim-bullseye

# apt-getの更新とユーザを追加
RUN apt-get update && apt-get -y install && \
    groupadd -g 1100 pyuser && \
    useradd -m -s /bin/bash -u 1100 -g 1100 pyuser

# ホスト上のrequirements.txtをコピー
COPY requirements.txt /home/pyuser

# 作業ディレクトリを変更
WORKDIR /home/pyuser

# パッケージをインストール
RUN pip install -r requirements.txt
```
Dockerイメージの軽量化のため、今回はslimを使用します。

`pyuser`はDocker Composeで実行ユーザとして使用します。


## 3. DBコンテナイメージの作成

`./docker/db_context/`に`Dockerfile`を作成します。

```dockerfile:Dockerfile
# 元イメージを定義
FROM mysql:8.3.0

# ユーザを追加 （mysqluser）
RUN groupadd -g 1100 mysqluser && \
    useradd -m -s /bin/bash -u 1100 -g 1100 mysqluser
```
`mysqluser`はDocker Composeで実行ユーザとして使用します。


## 4. Docker Composeの作成

Docker Composeで先ほど作成したイメージを使ってコンテナを構築します。

ルートディレクトリに`docker-compose.yml`を作成します。

```yaml:docker-compose.yml
# Docker Composeのバーション
version: '3'

# サービスの定義
services:
  # Webコンテナ
  web:
    container_name: web
    build:
      context: ./docker/web_context/
      dockerfile: ./Dockerfile
    user: node
    working_dir: /home/node/web
    volumes:
      - ./web:/home/node/web
    ports:
      - 3000:3000
    tty: true
    depends_on:
      - app
    networks:
      - client

  # Appコンテナ
  app:
    container_name: app
    build:
      context: ./docker/app_context/
      dockerfile: ./Dockerfile
    user: pyuser
    working_dir: /home/pyuser/app
    volumes:
      - ./app:/home/pyuser/app
    command: uvicorn api:app --host=0.0.0.0 --reload
    ports:
      - 8000:8000
    tty: true
    depends_on:
      - db
    networks:
      - client
      - server

  # DBコンテナ
  db:
    container_name: db
    build:
      context: ./docker/db_context/
      dockerfile: ./Dockerfile
    user: mysqluser
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: sample_db
      MYSQL_USER: sample_user
      MYSQL_PASSWORD: sample_user
    volumes:
      - ./docker/db_context/init:/docker-entrypoint-initdb.d
    ports:
      - 3006:3006
    networks:
      - server

networks:
  client:
    driver: bridge
  server:
    driver: bridge
```


### Webコンテナ

イメージは`./docker/web_context/Dockerfile`を使用するので、
ビルド情報のコンテキストは`./docker/web_context/`のディレクトリを指定します。

作業ユーザはNodeイメージには`node`というユーザが用意されているので、それを使います。
作業ディレクトリは`node`ユーザのホームディレクトリである`./home/node/`の中に`web/`というディレクトリを作成して使用します。
`working_dir: /home/node/web`と記述すると、`web`ディレクトリが存在しないときは、自動で作成してくれます。

ボリュームマウント（ `volumes: - ./web:/home/node/web` ）することで、ファイルを永続化しています。

ポート番号はReactを想定して、`3000`にしています。Vue.jsは`ports: -8080:8080`など、適宜変更してください。

WebコンテナはAppコンテナのAPIと通信する想定のため、Appコンテナが起動してから、Webコンテナを起動するようにします（　`depends_on: - app`　）。

また、WebコンテナがDBコンテナと通信できないようにするため、`client`というネットワークを作り、Appコンテナにだけ接続するようにしています。


### Appコンテナ

FastAPIを使用しているので、コンテナ起動時にサーバが立ち上がるようにするため、`command: uvicorn api:app --host=0.0.0.0 --reload`を設定しています。
コンテナ起動時に、APIが作成されていないと、このコマンドでエラーが発生するので、FastAPIで簡単なコードを作成しておきます。

`./app/api.py`を作成します。
```python:api.py
from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root():
    return {"Hello": "World"}
```

AppコンテナはDBコンテナとデータのやり取りとするので、DBコンテナが起動してから、Appコンテナが起動するようにしています（ `depends_on: - db` ）。

また、AppコンテナはWebコンテナ、DBコンテナ両方と通信するため、両方のネットワークと接続するようにしています。


### DBコンテナ

環境変数でMySQLの設定を追加しています。
* `MYSQL_ROOT_PASSWORD`
    ルートユーザのパスワード
* `MYSQL_DATABASE`
    データベースの作成
* `MYSQL_USER`
    ユーザの作成
* `MYSQL_PASSWORD`
    作成したユーザのパスワード


#### 初期データの作成
MySQLでは、`docker-entrypoint-initdb.d`ディレクトリにソースコードを置くことで、コンテナ起動時に自動で初期データを作成することができます。

今回はホスト上の`./docker/db_context/init/`にソースコードを作成して、コンテナ内の`docker-entrypoint-initdb.d`とボリュームマウントさせて、初期データを作成します。

`./docker/db_context/init/init.sql`を作成します。

初期データの例
```sql:init.sql
use sample_db;

create table sample_table(
    id integer not null,
    name char(20) not null,
    primary key (id)
    );

insert into sample_table(
    id,name
)
values(
    1,"sample_user"
);
```

### ネットワークの作成
* clientネットワーク
Webコンテナ - Appコンテナ間の通信に使用します。

* serverネットワーク
Appコンテナ - DBコンテナ間の通信に使用します。

## 5. コンテナの起動
以下のコマンドでコンテナを起動します。
```bash
$ docker compose up --build
```

## 6. コンテナ間の通信

### Webコンテナ - Appコンテナ

WebコンテナからAppコンテナにあるAPIを叩いてみます。

1. `./web/sample.js`を作成
Dockerではコンテナ間でネットワークが繋がっているとサービス名を使って名前解決してくれます。
WebコンテナからAppコンテナはネットワークが繋がっているので、
URLは`http://app`、ポート番号はFastAPIのデフォルトポート番号の8000を使います。
＊　ブラウザで動かす時（ ReactやVue.jsなど ）は名前解決してくれないので、`http://localhost:8000/`を使います。
```js:sample.js
const appUrl="http://app:8000";

const fetchApp=async()=>{
    const response= await fetch(appUrl);
    const data=response.json();
    return data;
};

(async()=>{
    try{
        const data= await fetchApp();
        console.log(data);
    }catch(error){
        console.log(error);
    };
})();
```

2.Webコンテナに入る
```bash
$ docker compose exec web bash
```

3. Webコンテナ内で`sample.js`を実行
```bash
$ node sample.js
```
`{"Hello":"World"}`がコンソールに表示されれば成功！


### Appコンテナ - DBコンテナ

AppコンテナでDBコンテナからデータを取り出します。

1. `./app/`に`con_db.py`を作成
```python:con_db
import mysql.connector

cnx = mysql.connector.connect(
    user='sample_user',
    password='sample_user',
    host='db',
    database='sample_db')

with cnx.cursor() as cursor:
    result = cursor.execute("SELECT * FROM sample_table")
    rows = cursor.fetchall()
    print("sample_table")
    for rows in rows:
        print(rows)
```

2. Appコンテナに入る
```bash
$ docker compose exec app bash
```

3. `con_db.py`を実行
```bash
$ python3 con_db.py
```
```
sample_table
(1, 'sample_user')
```
がコンソールに表示されれば成功！


# まとめ
Docker Composeで Javascript・Python ( FastAPI )・MySQLのコンテナ作成と通信方法についてまとめました。

全体のソースコードは[GitHub](https://github.com/ayakakawabe/docker-three-tiered-architecture)を参照してください！


## 参考資料
* [Docker Hub - node](https://hub.docker.com/_/node)
* [Docker Hub - Python](https://hub.docker.com/_/python)
* [Docker Hub - MySQL](https://hub.docker.com/_/mysql)
* [FastAPI](https://fastapi.tiangolo.com/ja/)
* [Docker Compose入門 (4) ～ネットワークの活用とボリューム～](https://knowledge.sakura.ad.jp/26522/)