🐳

1章 コピペに頼らない。ちゃんと理解するDocker(docker-compose)構築:Next.jsとNest.jsの環境構築

2025/02/03に公開

前置き

Dockerでプロジェクトを作成する際の環境構築で、公式ドキュメントを見たり、色々な記事を見ることになると思う。
コピペで良い感じに動くのもありつつ、色々な記事を見ると手順が変わるのもあり、ちゃんと分かってないと混乱することになる。
今回は、私自身が今後スムーズにDockerで環境を構築できるように
汎用的な方法として残しておこうと思う。

例を挙げるとNest.jsの公式ドキュメントの導入では、次のコマンドを実行するように記述されている

$ npm i -g @nestjs/cli
$ nest new project-name

Dockerで環境構築をやろうとすると、このようなコマンドは直接、そのまま打たないことが多い。
次のようにDockerの立ち上げ時にオプションで指定したりする。

$ docker-compose run nodejs npx @nestjs/cli new project-name

記事にあるソースをある程度コピペしても良い感じに動くことも多いが、行き詰って別の記事を見ると前提が違ったりで、混乱することもある。

それで今回は、プロジェクトを開始するにあたって、docker-compose.ymlファイルの基本構成や
イメージの選択から丁寧にやっていこうと考えたので、軌跡として残す。

コピペしているとどんな問題が起こり得るのか

例えば、Dockerfileのベースイメージとして、次の記述があったとする。

FROM node:20-alpine

これはnodeのベースイメージだが、alpineの記述がある。
nodeのベースイメージには、bullseyeやalpine、その他もあり、かなり雑に書くと次のような違いがある。

  • bullseye
    色々揃っててサイズ(容量)が大き目。
  • alpine
    最低限のライブラリしか入っておらず、非常に軽量。
    ライブラリが最低限なのもあって、セキュリティはbullseyeよりは良いらしい。
    しかし、ネイティブモジュールを使用するとなれば、素直には動いてくれないことがあるとのこと。
    ネイティブモジュールというと、CやC++を使用するライブラリ(libc等)を使用するモジュールを指すようだ。
    ネイティブモジュールを使用しないならalpineでよさそうだが、画像圧縮のためにsharp等のライブラリを使用する場合は注意が必要である。

sharp:https://github.com/lovell/sharp

このライブラリのpackage.jsonを見るとlibcを利用しているようなので、ネイティブモジュールと言える。
他には、パスワードなどの文字列のハッシュ化のライブラリの中にはネイティブモジュールもある。
勿論、必要なパッケージを随時いれることもできるし、問題を解決する方法も世の中に記事はあるが、これを知らずに開発を進めると、大変な思いをする可能性もある。

これからやっていくこと

  1. ベースイメージの選定
  2. docker-compose.ymlの作成
  3. Dockerfileの作成
  4. プロジェクトの初期化
  5. 5.Dockerfileとdocker-compose.ymlの修正

1.ベースイメージの選定

前述したように、今回のプロジェクトではNext.jsとNest.jsなのでNodeが必要になる。
Nodeを使用するなら下記のようなデータも見ていきたい。

https://nodejs.org/ja/about/previous-releases

2025/1/25現在、LTS(Long-Term Support)となっているのはNode.js 22である。
また画像を扱う必要もあり、ネイティブモジュールを使う可能性も大いにあり得る。
ということで、今回は22.13.1-bullseyeを利用する。
また、実行環境としてARM、X86-64の両方で動作できるように、対応するOS/ARCHも気にしておく。

https://hub.docker.com/_/node/tags?name=22.13.1-bullseye

2.docker-compose.ymlを作成する

ディレクトリ構造

project/
├── frontend/
├── backend/
└── docker-compose.yml 

各設定項目の内容をコメントに記述する

docker-compose.yml
# バージョン指定
version: '3.8'
# サービスを列挙する(docker psで確認できるコンテナ一覧の単位)
services:
  # サービス名
  frontend:
    # コンテナ名
    container_name: frontend
    # ソースからコンテナイメージを作成するためのビルド構成を指定する
    build:
      # Dockerfileを含むディレクトリへのパス、またはGitリポジトリへのURLを定義する。
      # 指定が無ければデフォルトの(./)となる。
      context: ./frontend
      # 代替Dockerfileを設定する(おそらくDockerfileの名前)
      dockerfile: Dockerfile
    # コンテナからアクセス可能なホストのパスを指定する
    volumes:
      # バインドマウントのパスを指定
      - ./frontend:/app
      # 名前付きボリュームを指定
      # - frontend-node-modules:/app/node_modules # 最初はコメントアウトしておく
    # ホストマシンとコンテナ間のポートマッピング
    ports:
      # ホストのポート:コンテナのポート
      - "3000:3000"
    # 割り当てられた stdin を使用して実行するようにサービスのコンテナーを構成する。
    # コンテナをSTDIN開いたままにして、標準入力を通じてコン​​テナに入力を送信できるようにする
    stdin_open: true
    # 疑似 TTY をコンテナに接続し、ターミナルをコンテナの I/O ストリームに接続する。
    # 疑似 TTY をコンテナに割り当てると、TTY デバイスが提供する入出力機能にアクセスできる
    # ようになる
    tty: true
    # サービスが相互に通信できるようになる
    # 詳細:https://docs.docker.com/reference/compose-file/networks/
    networks:
      # ネットワーク名
      - project_network

  backend:
    container_name: backend
    build:
      context: ./backend
      dockerfile: Dockerfile
    volumes:
      - ./backend:/app
      # - backend-node-modules:/app/node_modules # 最初はコメントアウトしておく
    ports:
      - "3001:3001"
    stdin_open: true
    tty: true
    depends_on:
      - db
    networks:
      - project_network

  db:
    container_name: project_mysql
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: project
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
    networks:
      - project_network
# 名前付きボリューム
volumes:
  mysql_data:
  # frontend-node-modules: # 最初はコメントアウトしておく
  # backend-node-modules: # 最初はコメントアウトしておく

# サービスが相互に通信できるようにする設定
networks:
  # 名前
  project_network:
    # ネットワーク名
    name: project_network
    #  デフォルトのネットワークドライバ
    #  詳細:https://docs.docker.com/engine/network/drivers/
    driver: bridge

Docker公式ドキュメント https://docs.docker.com/compose/gettingstarted/
docker-compose.yml リファレンス https://docs.docker.com/reference/compose-file/

3.Dockerfileを作成する

project/
├─ frontend/
│ └─Dockerfile
├─ backend/
│ └─Dockerfile
└─ docker-compose.yml

フロントエンド Next.js

フロントエンドはNext.jsなので、ベースイメージの起動だけにしておく

# frontend/Dockerfile
FROM node:22.13.1-bullseye
WORKDIR /app

バックエンド Nest.js

Nest.jsのプロジェクトのFirst stepsに次の記述があるので、ベースイメージの起動に加え、nestjs/cliのインストールだけはDockerのビルド時にやっておく

$ npm i -g @nestjs/cli
$ nest new project-name

https://docs.nestjs.com/first-steps

# backend/Dockerfile
FROM node:22.13.1-bullseye
RUN npm i -g @nestjs/cli
WORKDIR /app

4.プロジェクトの初期化

コンテナを起動する。

上記docker-compose.ymlを配置したディレクトリで次のコマンドを実行すると、コンテナが3つ起動する。

$ docker compose up

起動するコンテナ

$ docker ps
NAME              IMAGE                      COMMAND                  SERVICE    CREATED         STATUS         PORTS
backend    crew-management-backend    "docker-entrypoint.s…"   backend    8 seconds ago   Up 6 seconds   0.0.0.0:3001->3001/tcp
frontend   crew-management-frontend   "docker-entrypoint.s…"   frontend   8 seconds ago   Up 7 seconds   0.0.0.0:3000->3000/tcp
mysql      mysql:8.0                  "docker-entrypoint.s…"   db         8 seconds ago   Up 7 seconds   0.0.0.0:3306->3306/tcp, 33060/tcp

※ここから、"コンテナに入る"と記述した際は、次のコマンドを使用してコンテナと対話状態にすることを言う。

# 例
$ docker compose exec frontend bash
# node:23-alpine3.20等の場合はbashではなくshとなる

フロントエンド(Next.js)プロジェクト初期化

bashに入った状態で、プロジェクトを初期化するが
Dockerfile等、他のファイルがあると失敗するので、一度Dockerfileを別のディレクトリに移動しておき、プロジェクトの初期化後にDockerfileを戻す。
※最後の「.」を忘れずに。

# mv Dockerfile ../
# npx create-next-app@latest .
~~~~~
# mv ../Dockerfile ./

Next.jsの最新バージョンでプロジェクトを初期化する。
この時色々聞かれるので選択していく。

インストールするnextのパッケージのバージョン

Need to install the following packages:
create-next-app@15.1.6
Ok to proceed? (y)
カレントディレクトリ指定せずにnpx create-next-app@latestした場合、プロジェクト名を聞かれる

プロジェクト名

What is your project named? …

TypeScriptを使用するか

Would you like to use TypeScript? … No / Yes

ESLintを使用するか

Would you like to use ESLint? … No / Yes

CSSでTailwindを使用するか

Would you like to use Tailwind CSS? … No / Yes

プロジェクトのソースをsrc/ディレクトリに配置するかどうか
※pagesやappディレクトリをsrc配下に配置するかどうか。

Would you like your code inside a `src/` directory? … No / Yes

画面のルーティングにApp routerを使用するか
Next.js 13.4移行、安定板としてリリースされているので、使わない理由は特にないでしょう。

Would you like to use App Router? (recommended) … No / Yes

Turbopackを有効にするか
※記事執筆時2025/1/25では、まだ開発環境のみ対応

https://www.commte.co.jp/learn-nextjs/Turbopack

Would you like to use Turbopack for `next dev`? … No / Yes

インポートエイリアスの設定をカスタマイズするかどうか
デフォルトでは'@/*'が設定されているが、それを変更するか

Would you like to customize the import alias (`@/*` by default)? … No / Yes
What import alias would you like configured? … @/*

プロジェクトを起動して画面が立ち上がるか確認する。
※npmの場合

# npm run dev

ブラウザからlocalhost:3000にアクセスして、Next.jsの初期画面が表示されればOK

バックエンド(Nest.js)プロジェクト初期化

Nest.jsのプロジェクト初期化

nest new .

どのパッケージ管理ツールを使用するかを聞かれるので、お好きなパッケージ管理ツールを選択する。

Which package manager would you ❤ to use?
npm
yarn
pnpm

今回は、フロントエンドをポート3000番、バックエンドをポート3001番でDockerを起動しているので
main.tsのポート指定部分を変更する。

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
-  await app.listen(process.env.PORT ?? 3000);
+  await app.listen(process.env.PORT ?? 3001);
}
bootstrap();

プロジェクトを起動して、レスポンスを確認する。

# npm run start:dev

ブラウザからlocalhost:3001にアクセスしてHello World!が表示されればOK

VScode等のエディタから権限エラーによって編集できない場合は権限を変更する

projectディレクトリ配下の全てのファイルとディレクトリのユーザー、およびグループを変更する。
sudo chown -R <new_user>:<new_group> /path/to/project

5.Dockerfileとdocker-compose.ymlの修正

プロジェクトを作成したら、次回からはdocker compose up時にアプリが起動するように修正していく。

フロントエンドのDockerfileを修正する

FROM node:23-bullseye

WORKDIR /app

+# パッケージファイルのみをコピー
+COPY package*.json ./
+# 依存関係のインストール
+RUN npm install
+# ソースコードをコピー
+COPY . .
+# コンテナ起動時のコマンド
+CMD ["npm", "run", "dev"]

バックエンドのDockerfileを修正する

FROM node:23-bullseye

WORKDIR /app

RUN npm i -g @nestjs/cli

+# パッケージファイルのみをコピー
+COPY package.json package-lock.json ./
+# 依存関係のインストール
+RUN npm install
+# ソースコードをコピー
+COPY . .
+# コンテナ起動時のコマンド
+CMD ["npm", "run", "start:dev"]

docker-compoe.ymlの修正

今回作成したdocker-compose.yml

docker-compose.ymlの、以下のようにコメントアウトしていた部分を修正する。
    volumes:
      - ./frontend:/app
-      # - frontend-node-modules:/app/node_modules # 最初はコメントアウトしておく
+     - frontend-node-modules:/app/node_modules
~~~~~
    volumes:
      - ./backend:/app
-     # - backend-node-modules:/app/node_modules # 最初はコメントアウトしておく
+     - backend-node-modules:/app/node_modules
~~~~~
volumes:
  mysql_data:
-  # frontend-node-modules: # 最初はコメントアウトしておく
-  # backend-node-modules: # 最初はコメントアウトしておく
+   frontend-node-modules:
+   backend-node-modules:

node_modulesディレクトリは名前付きボリュームにする。
これをしないと、npm install後にローカルのnodu_modulesが上書きされ、moduleがインストールされていない状態になる。

dockerでのnode_modulesに関して
coming soon...

再度ビルドする

Dockerfileを修正したら再度ビルドする

$ docker compose up --build

これをしないとDockerfileの修正が反映されない。
次回から、docker compose downを実行した後にdocker compose upを実行すると、Dockerの起動と共にフロントエンド/バックエンドの両方が起動する。

次回はフロントエンドとバックエンドの疎通確認を行う。

Dockerで構築したNext.js、Nest.js、MySQL、Prismaで疎通確認を行う
comming soon...


by 株式会社DELTA
https://teamdelta.jp/


[CTO Booster]
https://costcut.cloud/

[VersionUp booster]
https://teamdelta.jp/lp/versionup


Discussion