こんにちは。金曜日になるとアニメ映画を観たくなります。Dockerおじさんです。
前回の記事で、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
コンテナの構成
今回は2パターンを試してみます。1
- ①Nginx, フロント, バック 全てが別コンテナに乗っかっているパターン
- ②Nginxとフロントを1コンテナにまとめており、バックは別コンテナのパターン
ディレクトリ構成
.
├── 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, フロント, バックエンドすべてを別のコンテナで動かします。
Nginxコンテナの準備
nginx.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を準備します。
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を作成します。
# ローカルで使っている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で走らせます。
フロントコンテナの動作確認
一応、動作確認してみましょう。
①以下のコマンドを実行します。
cd <プロジェクトのルートディレクトリ>/front
# コンテナイメージのビルド
docker build -t front .
# "front"というコンテナイメージを"front"という名前で起動
docker run -itd -p 3000:3000 --name front front
②ブラウザでhttp://(docker実行環境のホスト):80/にアクセス
バックエンドがいないので、ローディングアイコンがぐるぐるしていればOKです。
バックエンドコンテナの準備
Spring Bootのバックエンドコンテナを準備します。
- ソースからビルドしたい
- ビルドされたjarファイル名はプロジェクトごとに可変となるため、ビルド時に指定したい
- 最終的に実行するコンテナにはjarファイルだけがあればいい
という条件の下、マルチステージビルドを使います。2
以下のような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"]
バックエンドコンテナの動作確認
こちらも一応、動作確認してみましょう。
①以下のコマンドを実行します。
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にアクセス
curl http://localhost:8080/api/hello
{"jpn":"こんにちは","eng":"hello"}
挨拶文がJSONで返ってくれば成功です。
docker-compose.ymlの作成
最後に、上記3つのコンテナをまとめるdocker-compose.ymlを記述します。
プロジェクトのルートディレクトリに、以下のような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"ファイルを作成します。
JAR_FILE_PATH=/app/build/libs/demo-0.0.1-SNAPSHOT.jar
コンテナのビルド
docker-composeコマンドを使用し、コンテナを立ち上げます。
cd <プロジェクトのルートディレクトリ>
docker-compose up -d
動作確認
ブラウザで、http://(docker環境のホスト):80/にアクセスします。
挨拶文が返ってくればOKです。
②Nginxとフロントを1コンテナにまとめており、バックは別コンテナのパターン
コンテナの構成イメージ
フロントコンテナの準備
nginx.confの準備
front/nginx/conf配下に以下のようなnginx.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
# ローカルで使っている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を作成します。
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"ファイルを作成します。
JAR_FILE_PATH=/app/build/libs/demo-0.0.1-SNAPSHOT.jar
コンテナのビルド
docker-composeコマンドを使用し、コンテナを立ち上げます。
cd <プロジェクトのルートディレクトリ>
docker-compose up -d
動作確認
ブラウザで、http://(docker環境のホスト):80/にアクセスします。
挨拶文が返ってくればOKです。
-
全てを1コンテナにまとめることもできますが、1コンテナに3アプリが入ることになり、
障害に弱いのであまり実用的でない という観点からやらないことにしました。
この辺はいろんな派閥があると思いますが、悪しからず。。 ↩ -
実業務のCI/CD環境では、
①ソースがメインにマージされる
②ビルドツールが検知してjarファイル生成
③docker buildでは、それをコピーして起動するだけ のようなやり方が現実的かと考えます。 ↩ -
フロントにおいても、やはり実業務のCI/CD環境では、
①ソースがメインにマージされる
②ビルドツールが検知してnpm run build
③docker build では、②で出来上がったbuildディレクトリをコピーして起動するだけ が現実的かと考えます。 ↩