君（Spring bootのバックエンドとTypeScriptのフロントエンド）を（コンテナに）載せて

こんにちは。金曜日になるとアニメ映画を観たくなります。Dockerおじさんです。  
[前回の記事](https://qiita.com/fsdg-r-moriyama/items/eb8d93711ca5d257a8de)で、Nginxを使ってフロントとバックを同一ポートでアクセスできるようにしました。  
今回は、フロントとバックをそれぞれDockerコンテナに載せて起動させようと思います。

## 環境情報
- Docker version 20.10.17, build 100c70180f
- docker-compose version 1.21.2, build a133471
- nginx version: nginx/1.22.1
- Node.js v18.14.2 (npm 9.5.0)
- Java OpenJDK Runtime Environment Microsoft-7209853 (build 17.0.6+10-LTS)

なお、フロント / バックのソースコードは前回の記事と同じものを使用します。
- フロント: TypeScript + React + MUI
- バック: Java + Spring boot + gradle

## コンテナの構成
今回は２パターンを試してみます。[^1]
- ①Nginx, フロント, バック　全てが別コンテナに乗っかっているパターン
- ②Nginxとフロントを1コンテナにまとめており、バックは別コンテナのパターン

[^1]: 全てを1コンテナにまとめることもできますが、1コンテナに3アプリが入ることになり、  
障害に弱いのであまり実用的でない　という観点からやらないことにしました。  
この辺はいろんな派閥があると思いますが、悪しからず。。


## ディレクトリ構成
```sh:project
.
├── docker-compose.yml
├── front
│   ├── node_modules
│   ├── package.json
│   ├── package-lock.json
│   ├── public
│   ├── README.md
│   ├── src
│   │   ├── App.tsx
│   │   ├── index.tsx
│   │   ├── logo.svg
│   │   ├── react-app-env.d.ts
│   │   ├── reportWebVitals.ts
│   │   └── setupTests.ts
│   └── tsconfig.json
│
├── README.md
│
└── spring
    ├── build.gradle
    ├── gradle
    ├── gradlew
    ├── gradlew.bat
    ├── README.md
    ├── settings.gradle
    └── src
        ├── main
        │   ├── java
        │   │   └── ...
        │   └── resources
        │       └── application.yml
        └── test
            ├── java
            │   └── ...
            └── resources
                └── application.yml

```

ルートディレクトリにdocker-compose.ymlを置き、その下に  
"front" : フロントエンドのソース  
"spring" : バックエンドのソース  
を持ってきました。

## やってみよう
### ①Nginx, フロント, バック　全てが別コンテナに乗っかっているパターン
#### コンテナの構成イメージ
下図のように、Nginx, フロント, バックエンドすべてを別のコンテナで動かします。
![pattern1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3213461/a1f6ff63-76d9-8f27-ec3a-7aafc2e7c4f3.png)

#### Nginxコンテナの準備

##### nginx.confの記述
ルートディレクトリにnginx/conf/ディレクトリを作成し、以下のようなnginx.confを置きます。

```conf:./nginx/conf/nginx.conf
server {
    # Nginxがlocalhost:80でアクセスを受け付けます。
    listen       80;
    server_name  localhost;
    proxy_set_header    Host    $host;
    proxy_set_header    X-Real-IP    $remote_addr;
    proxy_set_header    X-Forwarded-Host       $host;
    proxy_set_header    X-Forwarded-Server    $host;
    proxy_set_header    X-Forwarded-For    $proxy_add_x_forwarded_for;

    # "http://<nginxコンテナ>:80/"配下のアクセスはfrontコンテナの3000番ポートに転送します。
    location / {
        proxy_pass    http://front:3000/;
    }

    # "http://<nginxコンテナ>:80/api/"配下のアクセスはspringコンテナの8080番ポートに転送します。
    location /api/ {
        proxy_pass    http://spring:8080/api/;
    }
}
```

##### Dockerfileの準備
以下のようなDockerfileを準備します。
```Dockerfile:nginx/Dockerfile
FROM nginx:1.22.1

# ベースイメージの設定ファイルを自前に置換
RUN rm /etc/nginx/conf.d/default.conf
COPY ./conf/nginx.conf /etc/nginx/conf.d

CMD ["nginx", "-g", "daemon off;"]
```

#### フロントコンテナの準備

frontディレクトリ直下に、以下のようなDockerfileを作成します。  
```dockerfile: ./front/Dockerfile
# ローカルで使っているNode.jsのバージョンと最も近いものを使用
FROM node:18.15.0

# コンテナの作業ディレクトリを/app配下に設定
WORKDIR /app

# /front配下すべてを/app配下に持っていく
COPY ./* /app/
COPY public/ /app/public/
COPY src/ /app/src/

# 必要なモジュールを全てインストール
RUN npm install

# コンテナ起動と同時に"npm start"が走る
ENTRYPOINT [ "npm", "start" ]
```

これをdocker-composeで走らせます。

##### フロントコンテナの動作確認
一応、動作確認してみましょう。
①以下のコマンドを実行します。
```sh: コマンド
cd <プロジェクトのルートディレクトリ>/front
# コンテナイメージのビルド
docker build -t front .
# "front"というコンテナイメージを"front"という名前で起動
docker run -itd -p 3000:3000 --name front front
```

②ブラウザでhttp://(docker実行環境のホスト):80/にアクセス  
バックエンドがいないので、ローディングアイコンがぐるぐるしていればOKです。
![loading.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3213461/d6ecde21-1ac0-3bfe-7f10-ee0f4ce6c7ba.png)

#### バックエンドコンテナの準備
Spring Bootのバックエンドコンテナを準備します。  
- ソースからビルドしたい
- ビルドされたjarファイル名はプロジェクトごとに可変となるため、ビルド時に指定したい
- 最終的に実行するコンテナにはjarファイルだけがあればいい

という条件の下、マルチステージビルドを使います。[^2]

[^2]: 実業務のCI/CD環境では、
①ソースがメインにマージされる
②ビルドツールが検知してjarファイル生成
③docker buildでは、それをコピーして起動するだけ　のようなやり方が現実的かと考えます。
  
以下のようなDockerfileを記述します。
```Dockerfile:spring/Dockerfile
# ソースのビルド用コンテナ
FROM mcr.microsoft.com/openjdk/jdk:17-ubuntu AS builder

# ソースを全て/app配下に持っていく
WORKDIR /app

COPY ./* ./
COPY ./gradle/ ./gradle/
COPY ./src/ ./src/

# sdkman!からgradleを入手→パスを通す
RUN apt-get update
RUN apt-get -y install curl
RUN apt-get -y install zip
RUN curl -s "https://get.sdkman.io" | bash
RUN echo ". $HOME/.sdkman/bin/sdkman-init.sh; sdk install gradle" | bash

# gradle build
RUN $HOME/.sdkman/candidates/gradle/current/bin/gradle build

# 実際に動かすコンテナの記述
FROM mcr.microsoft.com/openjdk/jdk:17-ubuntu

# gradle buildで生成される実行可能jarファイルのパス ファイル名をbuild-argで受け取る
ARG JAR_FILE_PATH="/app/build/libs/demo-0.0.1-SNAPSHOT.jar"

WORKDIR /app

# builderコンテナから実行可能jarファイルをコピー jarファイル名はbuild-argから受け取る
COPY --from=builder ${JAR_FILE_PATH} ./app.jar

ENTRYPOINT ["java", "-jar", "app.jar"]
```

##### バックエンドコンテナの動作確認
こちらも一応、動作確認してみましょう。
①以下のコマンドを実行します。
```sh: コマンド
cd <プロジェクトのルートディレクトリ>/spring
# コンテナイメージのビルド
# 例: docker build --build-arg JAR_FILE_PATH=<JARファイルのフルパス> -t spring .
# 生成されるjarファイル名が"demo-0.0.1-SNAPSHOT.jar"だと、以下のようになります。
docker build --build-arg JAR_FILE_PATH=/app/build/libs/demo-0.0.1-SNAPSHOT.jar -t spring .
# "spring"というコンテナイメージを"spring"という名前で起動
docker run -itd --name spring spring
```

②ブラウザでhttp://(docker実行環境のホスト):8080/api/helloにアクセス  
```sh:curl
curl http://localhost:8080/api/hello
{"jpn":"こんにちは","eng":"hello"}
```
挨拶文がJSONで返ってくれば成功です。

#### docker-compose.ymlの作成
最後に、上記3つのコンテナをまとめるdocker-compose.ymlを記述します。

プロジェクトのルートディレクトリに、以下のようなdocker-compose.ymlを作成します。

```yaml:docker-compose.yml
version: "3"
services:
  nginx:
    build: nginx #nginxディレクトリでコンテナをビルドする
    container_name: nginx
    ports:
      - 80:80 # 80番ポートを内外に公開

  front:
    build: front
    container_name: front
    restart: always
    expose:
      - "3000" # 3000番ポートを内部ネットワークに公開
    depends_on:
      - spring # springコンテナよりも後に起動させる

  spring:
    build:
      context: spring
      args:
        ${JAR_FILE_PATH}: /app/build/libs/demo-0.0.1-SNAPSHOT.jar
    container_name: spring
    restart: always
    expose:
      - "8080" # 8080番ポートを内部ネットワークに公開
```

#### .envファイルの作成
docker-compose.yml中の可変の値をすべて.envファイルに集約します。
docker-compose.ymlと同じ階層に以下のような".env"ファイルを作成します。
```:.env
JAR_FILE_PATH=/app/build/libs/demo-0.0.1-SNAPSHOT.jar
```

#### コンテナのビルド
docker-composeコマンドを使用し、コンテナを立ち上げます。
```sh:コマンド
cd <プロジェクトのルートディレクトリ>
docker-compose up -d
```

##### 動作確認
ブラウザで、http://(docker環境のホスト):80/にアクセスします。
![success.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3213461/1182b979-e57c-dade-f5f3-c5e300a4137e.png)
挨拶文が返ってくればOKです。


### ②Nginxとフロントを1コンテナにまとめており、バックは別コンテナのパターン
#### コンテナの構成イメージ
下図のような2コンテナの構成です。
![pattern2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3213461/2ce931d6-2654-7fa0-a000-aa774cfcf3bb.png)

#### フロントコンテナの準備
##### nginx.confの準備
front/nginx/conf配下に以下のようなnginx.confを作成します。
```conf
server {

  listen 80;

  location / {
    root   /usr/share/nginx/html;
    index  index.html index.htm;
    try_files $uri $uri/ /index.html;
  }

  location /api/ {
      proxy_pass http://spring:8080;
      proxy_http_version 1.1;
      proxy_set_header Upgrade $http_upgrade;
      proxy_set_header Connection 'upgrade';
      proxy_set_header Host $host;
      proxy_cache_bypass $http_upgrade;
   }

  error_page   500 502 503 504  /50x.html;

  location = /50x.html {
    root   /usr/share/nginx/html;
  }
}
```
##### Dockerfileの準備
マルチステージビルドを用いて、フロントコンテナを準備します。[^3]

[^3]: フロントにおいても、やはり実業務のCI/CD環境では、
①ソースがメインにマージされる
②ビルドツールが検知してnpm run build
③docker build では、②で出来上がったbuildディレクトリをコピーして起動するだけ　が現実的かと考えます。

```Dockerfile:front/Dockerfile
# ローカルで使っているNode.jsのバージョンと最も近いものを使用
FROM node:18.15.0 AS builder

# コンテナの作業ディレクトリを/app配下に設定
WORKDIR /app

# /front配下すべてを/app配下に持っていく
COPY ./* /app/
COPY public/ /app/public/
COPY src/ /app/src/

# 必要なモジュールを全てインストール
RUN npm install

# フロントのソースをビルド
RUN npm run build

# 実際に動かすコンテナ
FROM nginx:1.22.1

# ベースイメージの設定ファイルを自前に置換
RUN rm /etc/nginx/conf.d/default.conf
COPY ./nginx/conf/nginx.conf /etc/nginx/conf.d

# ビルドされたフロントのソースを持っていく
COPY --from=builder /app/build/ /usr/share/nginx/html

CMD ["nginx", "-g", "daemon off;"]
```

#### バックエンドコンテナの準備
バックエンドコンテナは[パターン①](#バックエンドコンテナの準備)と同じです。


#### docker-compose.ymlの作成
最後に、上記3つのコンテナをまとめるdocker-compose.ymlを記述します。

プロジェクトのルートディレクトリに、以下のようなdocker-compose.ymlを作成します。

```yaml:docker-compose.yml
version: "3"
services:
  front:
    build: front # frontディレクトリでコンテナをビルドする
    container_name: front
    restart: always
    ports:
      - 80:80 # 80番ポートを内外に公開
    depends_on:
      - spring # springコンテナよりも後に起動させる

  spring:
    build:
      context: spring
      args:
        - ${JAR_FILE_PATH}: /app/build/libs/demo-0.0.1-SNAPSHOT.jar
    container_name: spring
    restart: always
    expose:
      - "8080" # 8080番ポートを内部ネットワークに公開

```

#### .envファイルの作成
docker-compose.yml中の可変の値をすべて.envファイルに集約します。
docker-compose.ymlと同じ階層に以下のような".env"ファイルを作成します。
```:.env
JAR_FILE_PATH=/app/build/libs/demo-0.0.1-SNAPSHOT.jar
```

#### コンテナのビルド
docker-composeコマンドを使用し、コンテナを立ち上げます。
```sh:コマンド
cd <プロジェクトのルートディレクトリ>
docker-compose up -d
```

##### 動作確認
ブラウザで、http://(docker環境のホスト):80/にアクセスします。
![success.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3213461/1182b979-e57c-dade-f5f3-c5e300a4137e.png)
挨拶文が返ってくればOKです。