9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

React(TypeScript) × NestJS × PostgreSQL × docker-compose開発環境 個人的ベストプラクティス

Last updated at Posted at 2022-11-13

こんにちは、katです。
今回は「React(TS) × NestJS × PostgreSQL × docker-compose の開発環境」について、個人的に最も使いやすいと感じている構成を紹介してみたいと思います。

ちょうど1年前あたりから「docker-composeを用いたSPAの開発環境はどのような構成が使いやすいか…」と試行錯誤してきたのですが、ようやく満足のいくものが出来たため、本記事を投稿してみました。

※ 最初にお断りしておきますが、筆者はDockerを独学や学生時代の研究で2年近く使っていますが、実務で使ったことはありません。そのため、誤っている箇所や、改善したほうが良い点がございましたらご指摘していただけると幸いです。

前提

皆様の環境にDocker, docker-compose, Visual Studio Codeがインストールされている前提でお話します。
かつ、VSCodeにDev Containersという拡張機能がインストールされている必要があります(Remote Containerを使うため)。
また可能であれば、GNU Makeがインストールされているとなお良しです。

バージョンは、筆者の環境では以下のようになっております。

名称 バージョン
Docker 20.10.10
docker-compose 1.29.2
VSCode 1.73.1
Dev Containers 0.262.3
GNU Make 3.81

ソースコード解説

本記事作成時点でのGithubリポジトリはこちらになります。

ディレクトリ構成

ルート位置にdocker-compose.ymlを配置し、各フォルダ内にDoekerfileを配置しています。
よくある構成かと思います。

.
├── docker-compose.yml
├── Makefile
├── README.md
├── .gitignore
├── .env (各自作成してください)
├── front
│     └── Dockerfile
│     └── .devcontainer
│     │       └── devcontainer.json
│     └── Reactプロジェクト(初回起動時は空)
├── api
      └── Dockerfile
      └── .devcontainer
      │       └── devcontainer.json
      └── NestJSプロジェクト(初回起動時は空)

docker-compose.yml

各行にコメントアウトで解説を付けてあります。
なお、React, NestJS, PostgreSQL は以下のようなサービス名で立ち上がります。

名称 サービス名
React front
NestJS api
PostgreSQL db
docker-compose.yml
version: "3.8"

services:
  # React環境に関する設定
  front:
    build:
      # frontディレクトリ内のDockerfileをビルドする
      context: ./front
      # コンテナ側起動時のディレクトリ位置を/workspace/frontとする
      args:
        - FRONT_WORKDIR=/workspace/front
    # 下記2行はコンテナを起動させ続けるため記載
    tty: true
    stdin_open: true
    # マウントに関する設定
    # ホスト側のルート位置(docker-compose.ymlが格納されている階層)を、コンテナ側 /workspace にバインドマウント
    # コンテナ側のReactプロジェクトのnode_modulesを、front_storeにボリュームマウント
    volumes:
      - .:/workspace
      - front_store:/workspace/front/$FRONT_PROJ_NAME/node_modules
    # React(vite)は5173番ポートで起動するため、ローカル側のものをポートフォワード
    ports:
      - "5173:5173"
    # docker-compose起動時コマンド。プロジェクトディレクトリがある場合はアプリを起動し、ない場合はメッセージを出して待機
    command: >
      sh -c 
        "if [ -e $FRONT_PROJ_NAME/package.json ]; then 
          cd $FRONT_PROJ_NAME && yarn install && yarn dev --host
        else
          echo 'Project Directory is not found...' && tail -f /dev/null
        fi" 

  # NestJSに関する設定
  api:
    build:
      # apiディレクトリ内のDockerfileをビルドする
      context: ./api
      # コンテナ側起動時のディレクトリ位置を/workspace/apiとする
      args:
        - API_WORKDIR=/workspace/api
    # 下記2行はコンテナを起動させ続けるため記載
    tty: true
    stdin_open: true
    # マウントに関する設定
    # ホスト側のルート位置(docker-compose.ymlが格納されている階層)を、コンテナ側 /workspace にバインドマウント
    # コンテナ側のNestJSプロジェクトのnode_modulesを、api_storeにボリュームマウント
    volumes:
      - .:/workspace
      - api_store:/workspace/api/$API_PROJ_NAME/node_modules
    # NestJSは3000番ポートで起動するため、ローカル側のものをポートフォワード
    # また、Prisma Studioを使う際は5555番ポートで起動するため、ローカル側のものをポートフォワード
    ports:
      - "3000:3000"
      - "5555:5555"
    # docker-compose起動時コマンド。プロジェクトディレクトリがある場合はアプリを起動し、ない場合はメッセージを出して待機
    command: >
      sh -c 
        "if [ -e $API_PROJ_NAME/package.json ]; then 
          cd $API_PROJ_NAME && yarn install && yarn start:dev
        else
          echo 'Project Directory is not found...' && tail -f /dev/null
        fi"
    # db(Postgres)が起動してから起動するようにする。
    depends_on:
      - db

  db:
    image: postgres:12-alpine
    restart: always
    environment:
      TZ: Asia/Tokyo
      POSTGRES_DB: $DB_NAME
      POSTGRES_USER: $DB_USER
      POSTGRES_PASSWORD: $DB_PASS
    volumes:
      - db_store:/var/lib/postgresql/data
    ports:
      - 5432:5432

# 今回使うボリューム
volumes:
  db_store:
  front_store:
  api_store:

Dockerfile

Dockerfileについても、各行に解説コメントを付けました。

  • front(React)側
front/Dockerfile
FROM node:16-alpine
# docker-compose側から、コンテナ起動時のディレクトリ位置を引数として受け取り、そこで起動するよう設定
ARG FRONT_WORKDIR
WORKDIR ${FRONT_WORKDIR}
# 時刻は、日本時間のものに設定しておく
ENV TZ Asia/Tokyo
# dockerイメージのビルド時に実行されるコマンド
# パッケージマネージャapkのアップデート
RUN apk update
RUN apk upgrade
# gitはインストールしておきたい。また、git rebase等でvimも必要になるのでインストール
RUN apk add git vim
  • api(Nest)側
api/Dockerfile
# 基本的にはfrontのものと同じ
FROM node:16-alpine
ARG API_WORKDIR
WORKDIR ${API_WORKDIR}
ENV TZ Asia/Tokyo
RUN apk update
RUN apk upgrade
RUN apk add git vim
# NestJSをグローバルインストールしておく
RUN yarn global add @nestjs/cli

devcontainer.json

VSCode Remote Containerのための設定ファイルです。これにより、コンテナ内での作業をVSCodeで行うことが出来ます。

  • front(React)側
front/.devcontainer/devcontainer.json
{
	"name": "front-vscode",
    // docker-compose.ymlのfrontにアタッチする
    "dockerComposeFile": "../../docker-compose.yml",
	"service": "front",
    // 起動ディレクトリの設定
	"workspaceFolder": "/workspace/front",
    // 拡張機能の設定(日本語設定、git、Prettier、ESLint、Reactに関する拡張機能を設定)
    // 拡張機能の設定欄は、各自カスタマイズしてOKです。
	"customizations": {
		"vscode": {
			"extensions": [
				"MS-CEINTL.vscode-language-pack-ja",
				"mhutchie.git-graph",
				"esbenp.prettier-vscode",
				"dbaeumer.vscode-eslint",
				"dsznajder.es7-react-js-snippets"
			]
		}
	},
	"shutdownAction": "none"
}
  • api(Nest)側
api/.devcontainer/devcontainer.json
// 基本的にはfrontのものと同じ
{
	"name": "api-vscode",
	"dockerComposeFile": "../../docker-compose.yml",
	"service": "api",
	"workspaceFolder": "/workspace/api",
    // 拡張機能の設定(日本語設定、git、Prettier、ESLint、Prismaに関する拡張機能を設定)
    // 拡張機能の設定欄は、各自カスタマイズしてOKです。
	"customizations": {
		"vscode": {
			"extensions": [
				"MS-CEINTL.vscode-language-pack-ja",
				"mhutchie.git-graph",
				"esbenp.prettier-vscode",
				"dbaeumer.vscode-eslint",
				"Prisma.prisma"
			]
		}
	},
	"shutdownAction": "none"
}

使い方

envファイル準備

まず、プロジェクトのルート位置(docker-compose.ymlやMakefileがある位置)に、.envを作成します。
そこに、以下のように環境変数を記載します。

  • FRONT_PROJ_NAME=frontのプロジェクト名
  • API_PROJ_NAME=apiのプロジェクト名
  • DB_NAME=dbで使用するデータベース名
  • DB_USER=dbで使用するユーザー名
  • DB_PASS=dbで使用するパスワード

例としてはこのような感じで良いと思います。

.env
FRONT_PROJ_NAME=react_app
API_PROJ_NAME=nest_app
DB_NAME=postgres
DB_USER=kat
DB_PASS=P@ssw0rd

もちろん、.envは.gitignoreに含まれていますのでコミットされません。

dockerイメージのビルド

[GNU Makeあり] $ make build
[GNU Makeなし] $ docker-compose build

これにより、front/Dockerfile、api/Dockerfileをもとにイメージがビルドされます。

起動

[GNU Makeあり] $ make up
[GNU Makeなし] $ docker-compose up

で環境が起動します。

初回起動時は、NestJS、Reactのプロジェクトがまだ作成されていないため、ターミナル上に「Project Directory is not found...」という記載が出るはずです。この時点ではReact, Nestともにアクセスできません。

次に説明する「React, NestJSプロジェクト新規作成」後に起動した場合は、以下のようにlocalhostでアクセスできます。

  • React → localhost:5173
    スクリーンショット 2022-11-13 18.40.14.png

  • NestJS → localhost:3000
    スクリーンショット 2022-11-13 18.40.30.png

React、NestJSプロジェクト新規作成

Reactは下記コマンドで作成できます。

[GNU Makeあり] $ make front-create-app
[GNU Makeなし] $ docker-compose exec front sh -c "yarn create vite <環境変数FRONT_PROJ_NAMEの値> --template react-ts"

Reactのプロジェクト作成の際は、「ターゲットディレクトリは空ではありません。続行しますか。」と聞かれますが、"y"と入力してください。Hyper 2022-11-13 16-32-03.png

NestJSは下記コマンドで作成できます。

[GNU Makeあり] $ make api-create-app
[GNU Makeなし] $ docker-compose exec api sh -c "nest new <環境変数API_PROJ_NAMEの値> --package-manager yarn --skip-install --skip-git"

作業方法(Remote Container使用)

上記の手順でdocker-composeを立ち上げたら、VSCodeでRemote Containerに入ります。
手順としては、まず左下の「Remote Development」ボタン押下後「Open Folder in Container」を開きます。
スクリーンショット 2022-11-13 16.45.58.png

次に、フォルダ選択のダイヤログが出てきますので、api もしくは front フォルダを開きます。
スクリーンショット 2022-11-13 16.49.00.png

これにて、Remote Containerが立ち上がり、コンテナ内の作業をVSCodeで出来るようになります。
スクリーンショット 2022-11-13 16.51.08.png

終了方法

[GNU Makeあり] $ make down
[GNU Makeなし] $ docker-compose down

で終了できます。

工夫したポイント

docker-compose.yml におけるコンテナ起動時のコマンド

React、NestJSプロジェクト(正確にはその中のpackage.json)がある場合は実行に移るが、無い場合は実行に移らず待機するよう場合分けしました。これにより、初回起動時に「まだプロジェクトが出来ておらず、実行できない」といったエラーを防止できますし、逐一docker-compose.ymlを書き換える必要もなくなります。

docker-compose.yml
version: "3.8"

services:
  front:
    
    # docker-compose起動時コマンド。
    # Reactディレクトリ内にpackage.jsonがある場合は、node_modulesがインストールされ、開発用サーバーが起動する。
    # ない場合はメッセージを出した後、「tail -f /dev/null」コマンドで待機する(これが無いとコンテナが終了してしまう)。
    command: >
      sh -c 
        "if [ -e $FRONT_PROJ_NAME/package.json ]; then 
          cd $FRONT_PROJ_NAME && yarn install && yarn dev --host
        else
          echo 'Project Directory is not found...' && tail -f /dev/null
        fi" 
    
  api:
    
    # docker-compose起動時コマンド。
    # NestJSディレクトリ内にpackage.jsonがある場合は、node_modulesがインストールされ、開発用サーバーが起動する。
    # ない場合はメッセージを出した後、「tail -f /dev/null」コマンドで待機する(これが無いとコンテナが終了してしまう)。
    command: >
      sh -c 
        "if [ -e $API_PROJ_NAME/package.json ]; then 
          cd $API_PROJ_NAME && yarn install && yarn start:dev
        else
          echo 'Project Directory is not found...' && tail -f /dev/null
        fi" 
     

node_modulesのマウント方法

React、Nestといったnode.jsのフレームワークは、プロジェクト直下にnode_modulesディレクトリを作り、そこにnode.jsパッケージを保存するため、今回のボリュームマウントをする必要は必ずしもなく、他のts,tsxファイルと一緒にバインドマウントするというやり方も可能ではあります。
ですが、バインドマウントをnode_modulesに行ってしまうと、yarn add等で新しくパッケージをインストールする際にとても時間がかかってしまいます(普通なら数秒で終わるはずのインストールが5分以上かかってしまったりします)。
そのため、ボリュームマウントを取り入れ、インストール速度の問題をクリアしています。

React、NestJSプロジェクト作成時のひと工夫

今回のようにnode_modulesをボリュームマウントする場合、React・Nestのプロジェク作成時に工夫が必要になります。
初回起動(プロジェクト作成前)の時点でコンテナ内にnode_modulesディレクトリ(中身は空)が存在している状態になるのですが、そこにNestJS、Reactのプロジェクトをスタンダードなやり方で作成しようとすると、「node_modulesディレクトリが既に存在します」という旨のエラーになってしまいます。
そのため、プロジェクト作成の際、node.jsパッケージのインストールをスキップする方法を取る必要があります。

  • React : ビルドシステムをcreate-react-appではなくViteを使うことで、パッケージのインストールをスキップできる。
  • NestJS : nest new時に --skip-install オプションをつけ、パッケージのインストールをスキップする。

また、スキップしたnode.jsパッケージのインストールは、次回のdocker-compose up時のコマンドで行われます。

ReactのビルドシステムにViteを使用

筆者は当初、create-react-appで作成したReactで作業をしていたのですが、規模が大きくなるにつれ開発用サーバーがダウンする現象が多発しました。そのため、ビルドシステムの方を、動作が高速と評判なViteに変えたところ、開発用サーバーが落ちる問題が解決しました。
また、上でも述べたとおり、Viteではプロジェクトの新規作成の際にnodeパッケージのインストールをスキップでき、まさに一石二鳥でした。

さいごに

以上、見て頂きありがとうございました。
今現在この開発環境をベースに、PDCAサイクルのWebアプリを個人開発しています。そちらも完成したらぜひともQiitaで公開したいと考えています。
今後とも宜しくお願い致します。

9
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?