NestJSをインストールしてDockerで動かすところまでやる



## 概要
NestJSをWindows環境にインストールするところから、Dockerファイルに固めてコンテナ環境上で動作させるところまでの手順を忘備録代わりに残します。

環境は下記のとおりです

|環境|バージョン|
|-|-|
|OS|Windows 11 Home 22H2|
|nodeバージョン|v18.13.0|
|npm バージョン|8.19.3|
|エディタ|Visual Studio Code (*.editorconfigとかフォーマッタとか便利に使う設定を行う)|

## EoLスケジュール・サポートされる機能の確認

node 18.xは2025/4/30までサポートが行われる予定です。
NodeJSのリリーススケジュールは以下のページに記載があります。
https://github.com/nodejs/release#release-schedule


NodeJSのバージョンとサポートされるECMAScriptの機能について下記のページに記載があります。
https://node.green/


## 環境構築
### voltaのインストール
nodeのバージョン管理ツールとして、「volta」をインストールします
https://docs.volta.sh/guide/getting-started

Windows Installationの項目の「download and run the Windows installer 」からインストーラーをダウンロードしてインストールできます。
※他のnodeのバージョン管理ツールが入っている場合は、先に削除する必要があります

### node/npmのインストール及び設定
下記コマンドを実行し、node18をインストールします
```
volta install node@18
```


以下参考になりそうな記事
* https://blog.frevo-works.co.jp/entry/2022/05/02/163432
* https://link-and-motivation.hatenablog.com/entry/2022/05/20/160000
* https://www.3ryu-engineer.work/windows-volta-nodejs/

下記コマンドでインストールされていることを確認
```
PS C:\Users\yuuri> node -v
v18.13.0
PS C:\Users\yuuri> npm -v
8.19.3
```

### nestCLIのインストール

下記コマンドでnestCLIをグローバルインストールします
```
npm i -g @nestjs/cli
```
https://docs.nestjs.com/#installation


## nestjsのプロジェクトの作成及び確認
### プロジェクトの作成
インストールしたnestCLIを使用して、nestjsのプロジェクトを作成します。
使用するpackage managerを聞かれるので、使用したいものを選んでプロジェクトの作成を行います。
```
nest new <project-name>
```

下記では`npm`を選択しました

```
nest new nest-sample-project
npm WARN deprecated sourcemap-codec@1.4.8: Please use @jridgewell/sourcemap-codec instead

added 250 packages, and audited 251 packages in 19s

41 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
npm notice
npm notice New major version of npm available! 8.19.3 -> 9.3.1
npm notice Changelog: https://github.com/npm/cli/releases/tag/v9.3.1
npm notice Run npm install -g npm@9.3.1 to update!
npm notice
PS C:\Users\yuuri\sorairo-architecture> nest new nest-sample-project
⚡  We will scaffold your app in a few seconds..

? Which package manager would you ❤️  to use? npm
CREATE nest-sample-project/.eslintrc.js (663 bytes)
CREATE nest-sample-project/.prettierrc (51 bytes)
CREATE nest-sample-project/nest-cli.json (171 bytes)
CREATE nest-sample-project/package.json (1950 bytes)
CREATE nest-sample-project/README.md (3340 bytes)
CREATE nest-sample-project/tsconfig.build.json (97 bytes)
CREATE nest-sample-project/tsconfig.json (546 bytes)
CREATE nest-sample-project/src/app.controller.spec.ts (617 bytes)
CREATE nest-sample-project/src/app.controller.ts (274 bytes)
CREATE nest-sample-project/src/app.module.ts (249 bytes)
CREATE nest-sample-project/src/app.service.ts (142 bytes)
CREATE nest-sample-project/src/main.ts (208 bytes)
CREATE nest-sample-project/test/app.e2e-spec.ts (630 bytes)
CREATE nest-sample-project/test/jest-e2e.json (183 bytes)

✔ Installation in progress... ☕

🚀  Successfully created project nest-sample-project
👉  Get started with the following commands:

$ cd nest-sample-project
$ npm run start


                          Thanks for installing Nest 🙏
                 Please consider donating to our open collective
                        to help us maintain this package.


               🍷  Donate: https://opencollective.com/nest

```

設定したプロジェクト名と同名のフォルダが作成され、その中にnestjsのプロジェクトが作成されていることが確認できます

```
 ls .\nest-sample-project\


    ディレクトリ: C:\Users\yuuri\sorairo-architecture\nest-sample-project


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----        2023/01/21      2:32                node_modules
d-----        2023/01/21      2:31                src
d-----        2023/01/21      2:31                test
-a----        2023/01/21      2:31            663 .eslintrc.js
-a----        2023/01/21      2:32            391 .gitignore
-a----        2023/01/21      2:31             51 .prettierrc
-a----        2023/01/21      2:31            171 nest-cli.json
-a----        2023/01/21      2:32         554915 package-lock.json
-a----        2023/01/21      2:31           1950 package.json
-a----        2023/01/21      2:31           3340 README.md
-a----        2023/01/21      2:31             97 tsconfig.build.json
-a----        2023/01/21      2:31            546 tsconfig.json
```

### アプリケーションの起動

プロジェクトのフォルダに移動し、下記コマンドを実行することでアプリケーションを起動します。
下記コマンドで起動した場合、ファイルに変更を加えた場合自動的にWebサーバーが再起動されます。
```
npm run start:dev
```

package.jsonのscriptの箇所には下記のものが定義されています(一部抜粋しています)
開発環境では `npm run start:dev` もしくは `npm run start:debug`を利用すればよさそうです

```
    "build": "nest build",
    "start": "nest start",
    "start:dev": "nest start --watch",
    "start:debug": "nest start --debug --watch",
    "start:prod": "node dist/main",
```


### Voltaでのnodeバージョンの固定
プロジェクトのフォルダ内で下記コマンドを実行し、このプロジェクトで利用するnodeのバージョンを固定します。

```
volta pin node@18
volta pin npm@8
```

package.json内へに下記のように使用するnodeのバージョンが追記されます。

```
  "volta": {
    "node": "18.13.0",
    "npm": "8.19.3"
  }
```

## エディタ絡みの環境設定
Visual Studio Codeにプラグインを追加し、.editorconfigによるエディタ設定の適用と保存時のフォーマッタによるコード整形を実行できるようにします。

ぼっちで開発するときはあまりきにしないのですが、複数人で開発するときはこのあたりが入っていないとgit関連の操作やレビューで大変なことになるので設定します。


### .editorconfig
EditorConfig for VS CodeプラグインをVSCodeへ追加します

https://marketplace.visualstudio.com/items?itemName=EditorConfig.EditorConfig

その後プロジェクトルートに`.editorconfig`ファイルを作成します。
``` .editorconfig
root = true
[*]
# インデント時のスタイル設定（tab/space）
indent_style = space
# インデント時のサイズ設定
indent_size = 2
# 改行コード設定（lf/cr/crlf）
end_of_line = lf
# 文字コード設定（latin1/utf-8/utf-8-bom/utf-16be/utf-16le/...）
charset = utf-8
# 文末スペースの設定（true=削除）
trim_trailing_whitespace = true
# 最終行の改行設定（true=改行あり）
insert_final_newline = true

```

以上で、ファイルを操作する際に`.editorconfig`の設定がエディタに反映されるようになります。

参考:  https://rfs.jp/sb/vsc/editorconfig.html

### フォーマッタなどの設定

#### デフォルト設定
デフォルトでprettierの設定ファイルである`.prettierrc`が作成されます。
```.prettierrc
{
  "singleQuote": true,
  "trailingComma": "all"
}
```
また`package.json`のscriptsのところへ下記設定が設定されています。
```
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",

```

そのため、`npm run format`コマンドを実行することでコードのフォーマットを行うことができます。

```
npm run format

> nest-sample-project@0.0.1 format
> prettier --write "src/**/*.ts" "test/**/*.ts"

src\app.controller.spec.ts 200ms
src\app.controller.ts 12ms
src\app.module.ts 7ms
src\app.service.ts 6ms
src\main.ts 4ms
test\app.e2e-spec.ts 11ms
```

#### VS Codeへプラグインの追加及び設定
ただ、毎回手動でフォーマットを実行するのは面倒くさいというかそのうちやらなくなるので、下記プラグインを追加しファイル保存時に自動的にフォーマッターが動作するようにします

https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode


ファイル>ユーザー設定>設定 からVScodeの設定画面を開き、左側のメニューからテキスト エディター>書式設定を選択します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213714/2644a108-ab41-85ca-cb59-8d4c59e770a9.png)
`Format On Paste`, `Format On Save`, `Format On Type`のチェックボックスへ要件に応じてチェックを入れます。
ファイル保存時にフォーマッタを動作させる場合、`Format On Paste`にチェックを入れます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213714/5949c59c-7357-58b6-2c5d-b69280945911.png)


その後、`Default Formatter`の項目で、`Prettier`をデフォルトのフォーマッタとして設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213714/a7a1f317-a6be-965f-2909-50f133688cb4.png)

これで、ファイル保存時などにpretterによるコードフォーマットが動作します。
.editorconfigの値はprettierからも参照(一部非対応で参照されないものもあるようですが)され、これに従ってフォーマットが行われるようです。

https://magicode.io/Sumiren/articles/29af3d78f68e41a9b251bb55dd62a44c


### Linterの設定
#### デフォルト設定
デフォルトでeslinterの設定ファイルである`.eslintrc.js`が作成されます。
下記コマンドを実行することで、linterを動作させることができます(自動的にコードが修正されます)。

```
npm run lint

> nest-sample-project@0.0.1 lint
> eslint "{src,apps,libs,test}/**/*.ts" --fix
```

#### VS Codeへプラグインの追加及び設定
下記プラグインをVS Codeに追加します。
https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint

プラグインを追加することで、linterによる構文検査結果をVScode上で確認することができます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213714/5e6e8493-3961-39b6-6c08-c4cb4847317c.png)

参考: 

https://zenn.dev/ianchen0419/articles/3c4644d5f20bdb

## 環境変数及び設定ファイルの作成

* ローカル環境での実行は.envファイルを読み込む
* その他環境での実行時は、環境変数から設定を読み込む

形で実装していきます。

### configurationモジュールの追加
下記コマンドでconfigurationモジュールを追加します
```
npm i --save @nestjs/config
```

`app.module.ts`内でconfigurationモジュールを読み込むよう設定を追加します
この例ではnest内のすべてのモジュールから利用できるように、`isGlobal:true`を設定しています。
https://docs.nestjs.com/techniques/configuration#use-module-globally
また`process.env`へのアクセス(configurationモジュールは内部でdotenvを利用しています)が遅くなる場合があるそうなので、`cache: true`を設定しています。
https://docs.nestjs.com/techniques/configuration#cache-environment-variables
``` app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ConfigModule } from '@nestjs/config'; //追加

@Module({
  imports: [
    ConfigModule.forRoot({ //追加
      isGlobal: true,      //追加
      cache: true,         //追加
    }),                    //追加
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

```


### 設定値の読み取り
動作確認用に.envファイルを設定しました。
``` .env
ENVIRONMENT=local
```

`app.service.ts`で、configServiceを読み込み、`.get('<環境変数名>')`メソッドを使うことで設定値を読み込むことができます。

``` app.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; //追加

@Injectable()
export class AppService {
  private valueFromEnvFile: string; //追加

  constructor(private configService: ConfigService) {
    this.valueFromEnvFile = this.configService.get<string>('ENVIRONMENT'); //追加
  }

  getHello(): string {
    return 'valueFromEnvfile: ' + this.valueFromEnvFile; //追加
  }
}

```

下記コマンドで開発用サーバーを起動します

``` bash
npm run start:dev
```
`localhost:3000`へアクセスして動作確認を行います。
`.env`ファイルの中身が表示されることが確認できました。
![image3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213714/8ecb4449-066e-2879-f3b1-80f432a48fd7.png)


#### 環境変数の利用
.envファイルと環境変数で同じ名前が設定されていた場合、環境変数の設定が優先して利用されます。
そのためローカル環境以外の環境では、環境変数を設定しておくことで`.env`がなくても環境変数の値を参照して動作させる、もしくは`.env`の値を環境変数で設定値を上書きして動作させることができます。

詳細はあとの章に記載しますが、例えばdocker-composeで環境変数を設定することで、`.env`から値を読み出すのと同様の手順で環境変数の値をプログラム内から使用することができます。
```docker-compose.yml
version: '3'
services:
  nest-sample-project:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - ./src:/usr/src/app/src
    ports:
      - 3001:3000
    environment:
      - ENVIRONMENT=docker #環境変数の設定
    command: ['npm', 'run', 'start:dev']

```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213714/03ede9d6-5099-663d-2068-704ab3e4d216.png)



参考: 

https://docs.nestjs.com/techniques/configuration

https://zenn.dev/waddy/articles/nestjs-configuration-service

https://qiita.com/piggydev/items/af083bafc01e6be5930d

https://www.npmjs.com/package/dotenv#should-i-have-multiple-env-files


## Docker環境の整備

* 開発環境用
    * Dockerfile.dev
    * docker-compose
* 本番環境用
    * Dockerfile


を用意します。
開発環境では、他のDockerイメージと連携させて動作させるような環境の開発環境構築を想定しています。(単体で動かすだけであれば`node run start:dev`すればいいかと思います)

本番環境はApp RunnerやCloud Run、K8sなどで動作させることを想定しています。

### `.dockerignore`の準備
プロジェクトのルートディレクトリに下記ファイルを用意します

```.dockerignore

node_modules
npm-debug.log
dist

Dockerfile
Dockerfile.dev
docker-compose.yml

.env

.editorconfig
.eslintrc.js
.prettierrc
README.md

.git
.gitignore
.dockerignore
```


### 開発環境の構築
参考: https://nodejs.org/ja/docs/guides/nodejs-docker-webapp/

プロジェクトルートにそれぞれファイルを配置します。

``` Dockerfile.dev
FROM node:18
ENV NODE_ENV development
WORKDIR /usr/src/app

COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000

CMD ["npm", "run","start:dev"]
```

```docker-compose.yml
version: '3'
services:
  nest-sample-project:
    build:
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - ./src:/usr/src/app/src
    ports:
      - 3000:3000
    environment:
      - ENVIRONMENT=docker
    command: ['npm', 'run', 'start:dev']

```

#### ビルド
下記コマンドでDockerイメージのビルドを実行します
```
docker-compose build
```

#### 起動
下記コマンドでdocker-composeで定義した開発環境をバックグラウンドで起動します。
```
docker-compose up -d
```

#### ログ確認

下記コマンドでログの確認を行えます
`-f`オプションを追加することで、継続的にログを確認できます。
その他にDocker Desktopの画面でログの確認等を行えます
```
docker-compose logs 
```

#### コンテナ内のシェルへアクセスする
下記コマンドでコンテナ内部のシェルへアクセスすることができます。
`nest-sample-project`の箇所がdocker-composeで指定したサービス名に対応します。
```
docker-compose exec nest-sample-project bash
root@c4393687a35c:/usr/src/app#
```
`exit`コマンドでシェルから抜けられます

#### 終了
下記コマンドで起動したコンテナを終了できます。
```
docker-compose down
[+] Running 2/2
 - Container nest-sample-project-nest-sample-project-1  Removed                                                                                                       1.0s
 - Network nest-sample-project_default                  Removed 
```


#### キャッシュを利用せずにビルドを行う

下記コマンドで
たまにキャッシュが悪さをするときがあるので、そういったときはこのコマンドを利用してコンテナを再ビルドします。
```
docker-compose build --no-cache
```


参考: https://pointsandlines.jp/server-infra/docker/docker-compose-build-no-cache

### 本番環境のDockerfileの作成

主にsnyk社が自社の技術ブログで公開している「10 best practices to containerize Node.js web applications with Docker」とnodejsのリポジトリにある「Docker and Node.js Best Practices」を参考に、色々試行錯誤しながら作成しました。

https://snyk.io/blog/10-best-practices-to-containerize-nodejs-web-applications-with-docker/

https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md



``` Dockerfile
# ビルド環境
FROM node:18.13.0-bullseye-slim as builder
WORKDIR /usr/src/app

## ビルド必要なパッケージをインストール
COPY package*.json ./
RUN npm install

## ビルドの実施
COPY . /usr/src/app
RUN npm run build

# 実行環境
FROM node:18.13.0-bullseye-slim
ENV NODE_ENV production
WORKDIR /usr/src/app

## ビルド環境からビルド済みのファイル等をコピーし、当該フォルダのオーナーをnodeユーザーへ変更
COPY --from=builder --chown=node:node /usr/src/app/ /usr/src/app/ 
## 動作に必要なパッケージのインストール
RUN npm ci --only=production
EXPOSE 3000

## nodeユーザーとして実行
USER node
CMD ["node", "dist/main"]
```

#### Dockerイメージのビルド及び動作確認

下記コマンドでDockerfileをビルドし、`prod-nest-sample-project` というタグを付与します
```
docker build . -t prod-nest-sample-project
```

下記コマンドで`prod-nest-sample-project`のタグがついたイメージを起動し、localhostの3000番ポートとコンテナの3000番ポートを接続します
```
docker run -p 3000:3000 -t prod-nest-sample-project
```

ブラウザでアクセスし、動作確認を行えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/213714/2f1f3572-ef44-b280-d67c-370e40b3d08c.png)

## (追記)nestjsの終了時の処理
コンテナの終了時などにOSからSIGTERMが発行され、これを受けてnestjsでプログラムの終了処理を実行することができます。
https://docs.nestjs.com/fundamentals/lifecycle-events#application-shutdown

このあたりの機能はデフォルトでは無効(メモリリソースを使う処理のため)になっていますが、 `main.ts`を下記のように変更することで有効にすることができます。

```main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Starts listening for shutdown hooks
  app.enableShutdownHooks(); //追加

  await app.listen(3000);
}
bootstrap();
```

で、上記機能を有効にすることで、きちんと`SIGTERM`を取り扱ってくれるようです。(ドキュメント等の記載では特に見つけられていないです・・・)
dockerを停止する際に10秒後の`SIGKILL`が発行されるまでコンテナが終了しなかったものが、上記処理を有効にすることで10秒待たずに終了するようになりました。



## (追記) huskyによるpre-commitの利用
[husky](https://typicode.github.io/husky/getting-started.html)を導入することで、`git commit`実行直前に任意のコマンド(`npm run format`とか)を実行することができます。  

[Automatic (recommended)](https://typicode.github.io/husky/getting-started.html#automatic-recommended)にしたがって、huskyを導入します。  
その後`.husky/pre-commit`を編集することで任意のコマンドを実行することができます

```bash
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm run format #追加
npm test
```