🕌

NestJSをDocker化する

2023/05/03に公開

概要

Dockerを使用して、開発環境と本番環境の両方でNestJSアプリを実行する方法について紹介します。

工夫ポイント

NestJSアプリをDockerで構築する際のいくつかの工夫ポイントを紹介します。

  • マルチステージビルド: イメージサイズを抑えるために、マルチステージビルドを使用します。これにより、開発環境と本番環境で必要なファイルのみをコピーし、不要なファイルを削除することができます。
  • tini: PID 1問題に対処するために、tiniを使用します。tiniは、シグナルの適切な処理やゾンビプロセスの回収など、PID 1プロセスとして適切に機能するためのツールです。
  • nodeユーザー: なるべくnodeユーザーを使用します。これにより、コンテナ内でrootユーザーとして実行されることを回避し、セキュリティを向上させます。
  • ローカルファイルのマウント: 開発時には、ローカルのファイルをコンテナにマウントして、変更をリアルタイムで反映させることができます。

使い方

以下に、開発環境と本番環境(ローカル検証時)でのコンテナの起動方法を示します。

  • 開発環境
docker compose up --build
  • 本番環境(ローカル検証時)
docker build . -t test
docker run --rm -e TZ=Asia/Tokyo -p 3000:3000 test

コード

以下に、NestJSアプリケーションをDockerで構築するために必要なDockerfileとdocker-compose.ymlを示します。

Dockerfile

Dockerfileは、マルチステージビルドを利用しています。開発用(development)、ビルド用(build)、本番用(production)の3つのステージで構成されています。

DEVELOPMENTステージ

Dockerfile
FROM node:20.0.0-slim AS development

# 作業ディレクトリを/usr/src/appに設定しています。
WORKDIR /usr/src/app

# tzdataはタイムゾーンデータを提供し、tiniはPID 1問題に対処するために使用されます。
RUN apt-get update && apt-get -qq install -y --no-install-recommends \
    tzdata \
    tini \
    && rm -rf /var/lib/apt/lists/*

# 環境変数NODE_ENVをdevelopmentに設定しています。
ENV NODE_ENV development

# package.jsonとpackage-lock.jsonをコピーし、所有者をnodeユーザーとnodeグループに設定しています。
COPY --chown=node:node package*.json ./

# package-lock.jsonに記載された依存関係をインストールしています。
RUN npm ci

# アプリケーションのソースコードをコピーし、所有者をnodeユーザーとnodeグループに設定しています。
COPY --chown=node:node . .

USER node

PRODUCTION BUILDステージ

Dockerfile
FROM node:20.0.0-slim As build

WORKDIR /usr/src/app

ENV NODE_ENV production

COPY --chown=node:node package*.json ./

# 開発ステージでインストールしたnode_modulesディレクトリをコピーし、所有者をnodeユーザーとnodeグループに設定しています。これにより、開発ステージでインストールされたすべての依存関係を利用できます。
COPY --chown=node:node --from=development /usr/src/app/node_modules ./node_modules

COPY --chown=node:node . .

# NestJSアプリケーションをビルドします。これにより、distディレクトリにトランスパイルされたJavaScriptファイルが生成されます。
RUN npm run build

# npm ciコマンドを使用して、開発用の依存関係を除いた本番用の依存関係をインストールします。また、npm cache clean --forceでnpmキャッシュを削除して、イメージサイズを削減します。
RUN npm ci --omit=dev && npm cache clean --force

USER node

PRODUCTIONステージ

Dockerfile
FROM node:20.0.0-slim

WORKDIR /usr/src/app

ENV NODE_ENV production

RUN apt-get update && apt-get -qq install -y --no-install-recommends \
    tzdata \
    tini \
    && rm -rf /var/lib/apt/lists/*

COPY --chown=node:node --from=build /usr/src/app/package.json ./
COPY --chown=node:node --from=build /usr/src/app/node_modules ./node_modules
COPY --chown=node:node --from=build /usr/src/app/dist ./dist

EXPOSE 3000

USER node

# エントリーポイントにtiniを指定し、--オプションを渡しています。これにより、tiniがPID 1として実行され、その後に実行されるプロセスが正しくシグナルを受け取ることができます。
ENTRYPOINT ["/usr/bin/tini", "--"]
# デフォルトのコマンドとして、トランスパイルされたmain.jsをnodeで実行します。これにより、NestJSアプリケーションが起動します。
CMD [ "node", "dist/main.js" ]

完成版

Dockerfile
###################
# DEVELOPMENT
###################
FROM node:20.0.0-slim AS development

WORKDIR /usr/src/app

RUN apt-get update && apt-get -qq install -y --no-install-recommends \
    tzdata \
    tini \
    && rm -rf /var/lib/apt/lists/*

ENV NODE_ENV development

COPY --chown=node:node package*.json ./

RUN npm ci

COPY --chown=node:node . .

USER node


###################
# PRODUCTION BUILD
###################

FROM node:20.0.0-slim As build

WORKDIR /usr/src/app

ENV NODE_ENV production

COPY --chown=node:node package*.json ./

COPY --chown=node:node --from=development /usr/src/app/node_modules ./node_modules

COPY --chown=node:node . .

RUN npm run build

RUN npm ci --omit=dev && npm cache clean --force

USER node


###################
# PRODUCTION
###################

FROM node:20.0.0-slim

WORKDIR /usr/src/app

ENV NODE_ENV production

RUN apt-get update && apt-get -qq install -y --no-install-recommends \
    tzdata \
    tini \
    && rm -rf /var/lib/apt/lists/*

COPY --chown=node:node --from=build /usr/src/app/package.json ./
COPY --chown=node:node --from=build /usr/src/app/node_modules ./node_modules
COPY --chown=node:node --from=build /usr/src/app/dist ./dist

EXPOSE 3000

USER node

ENTRYPOINT ["/usr/bin/tini", "--"]
CMD [ "node", "dist/main.js" ]

docker-compose.yml

  • appサービスとして、カレントディレクトリをコンテキストとし、Dockerfileを使用してビルドします。また、ターゲットステージとしてdevelopmentを指定しています。
  • ホストマシンとコンテナ間でソースコードを同期し、node_modulesディレクトリをコンテナ内に保持します。これにより、開発中にソースコードを変更した場合でも、リアルタイムで反映されます。
  • ホストマシンのポート5000をコンテナ内のポート3000にマッピングします。これにより、ホストマシンからアプリケーションにアクセスできます。
  • エントリーポイントにtiniを指定し、--オプションを渡しています。これにより、tiniがPID 1として実行され、その後に実行されるプロセスが正しくシグナルを受け取ることができます。
  • 開発サーバーを起動するためのコマンドnpm run start:devを実行しています。これにより、ソースコードの変更がリアルタイムで反映され、アプリケーションが自動で再起動されます。
docker-compose.yml
version: '3'

services:
  app:
    build:
      context: ./
      dockerfile: Dockerfile
      target: development
    volumes:
      - ./:/usr/src/app
      - /usr/src/app/node_modules
    ports:
      - "5000:3000"
    entrypoint:
      - /usr/bin/tini
      - --
    command: npm run start:dev

参考

https://www.tomray.dev/nestjs-docker-production#putting-it-all-together
https://dawchihliou.github.io/articles/the-last-dockerfile-you-need-for-nestjs
https://mzqvis6akmakplpmcjx3.hatenablog.com/entry/2020/11/10/231047
https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md

Discussion