Next.js のプロジェクトを作成して開発環境の Docker で動くようにする【その3】

Next.js のプロジェクトを作成して開発環境の Docker で動くようにする【その1】
Next.js のプロジェクトを作成して開発環境の Docker で動くようにする【その2】
続きです。

前回は Dockerfile を作成して Docker 環境上で動くようにしました。

今回は Docker Compose を利用して、より便利に Docker 環境で Next.js を動かすことを目標とします。

Docker Compose を使うメリット

前回の記事で、Docker 環境上で Next.js プロジェクトを動かすことができました。
まずは、Docker Compose を導入するメリットについて調査してみます。

Docker Compose の役割

Docker Compose と Dockerfile はそれぞれ役割が異なります。

Dockerfile は単にイメージを作成するもので、Docker Compose は コンテナ・ネットワーク・ボリュームを作成することができます。
Docker Compose ではコンテナを周辺環境と合わせて実行したり、停止・破棄します。
複数のイメージを利用して、コンテナを作成する場合にも1つのコマンドで実行することができます。Next.js のコンテナと MySQL のコンテナを起動したい等の複数のコンテナがある場合にも役立ちます。

(これは雰囲気で書いています。大体こんな感じのはず)

# イメージから作成する場合は多くのコマンドを打つ必要がある
$ docker network create donut-site-network
$ docker run --name donut-site donut-site-image
$ docker run --name donut-site-db -dit -e MYSQL_ROOT_PASSWORD=rootpass mysql

# Docker Compose を使う場合
$ docker compose up

導入の理由

実際 Docker Compose を使わなくても同じ挙動を行わせることはできますが、環境変数の設定なども含めると、使わない選択肢はないって感じがしますね。

以下の理由より Docker Compose の導入を行っていこうと思います。

  • 環境変数などの設定を定義ファイルにまとめることができる
  • network 等の周辺環境も同時に用意することができる
  • コマンドの手打ちによるミスを防ぐことができる

Docker Compose の導入

Docker Compose は、Docker Compose V2以降 Docker コマンドとして利用できるようになっているので、特にインストール等はしなくても使えるようになっています。

定義ファイル(docker-compose.yml)を用意することで、利用することができます。

docker-compose.ymlを用意する

現在ディレクトリは以下の構成になっています。

donuts-site
├── Dockerfile
├── README.md
├── dev.Dockerfile
├── next-env.d.ts
├── next.config.js
├── node_modules
├── package.json
├── pages
├── public
├── styles
├── tsconfig.json
└── yarn.lock
STEP.1
docker-compose.ymlを用意する
Next.js 公式の docker-compose サンプルを参考に、開発環境用にdocker-compose.dev.ymlを用意します。

# 作業ディレクトリに移動
$ cd ~/NextProjects/donuts-site

# 開発環境用の定義ファイルを作成する
$ touch docker-compose.dev.yml
STEP.2
定義を書く

前回作成したdev.Dockerfileを元に作られたイメージを利用してコンテナを起動するような定義を書きます。

docker-compose.dev.yml
version: '3'

services:
  donuts-site:
    container_name: donuts-site
    build:
      context: .
      dockerfile: dev.Dockerfile
    ports:
      - 3000:3000
contextの指定

Docker のビルドコンテキストの指定を行います。

docker buildコマンドを実行したときの、カレントなワーキングディレクトリのことを ビルドコンテキスト(build context)と呼びます。 デフォルトで Dockerfile は、カレントなワーキングディレクトリにあるものとみなされます。 ただしファイルフラグ(-f)を使って別のディレクトリとすることもできます。 Dockerfileが実際にどこにあったとしても、カレントディレクトリ配下にあるファイルやディレクトリの内容がすべて、ビルドコンテキストとして Docker デーモンに送られることになります。

この指定がない場合には予期せぬバグが発生することもあるので指定しておくとよさそうです。
(イメージが複数作られてしまうバグ?が発生しました。)

STEP.3
実行する

docker composeコマンドを実行してコンテナの起動をしてみます。

# ビルドを行う docker compose -f docker-compose.yml ファイルの指定 build
$ docker compose -f docker-compose.dev.yml build

# 起動する docker compose -f docker-compose.yml ファイルの指定 up -d(バックグラウンドで起動)
$ docker compose -f docker-compose.dev.yml up -d

http://localhost:3000/ にアクセスして無事にページが表示されました。

詳細の確認

buildコマンドではイメージの作成を行って、upコマンドではコンテナの起動を行っています。

# ビルドを行う docker compose -f docker-compose.yml ファイルの指定 build
$ docker compose -f docker-compose.dev.yml build

# イメージが作成されたことを確認する
$ docker image ls

REPOSITORY TAG IMAGE ID CREATED SIZE
donuts-site_donuts-site latest bf9af7aee505 4 minutes ago 916MB

# この時点ではコンテナは生成されていない
$ docker container ls -a

# 起動する docker compose -f docker-compose.yml ファイルの指定 up -d(バックグラウンドで起動)
$ docker compose -f docker-compose.dev.yml up -d

# コンテナを確認(-a オプションなしの場合は起動されているコンテナのみを表示)
$ docker container ls

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
eb6438e48db8 donuts-site_donuts-site "docker-entrypoint.s…" 17 seconds ago Up 8 seconds 0.0.0.0:3000->3000/tcp donuts-site

STEP.4
後始末
開発環境では利用しない時はコンテナは削除しておきたいので、後始末をします。

# コンテナを止める
$ docker compose -f docker-compose.dev.yml down

# コンテナが削除されていることを確認
$ docker container ls -a

# イメージは残っていることを確認
$ docker image ls

REPOSITORY TAG IMAGE ID CREATED SIZE
donuts-site_donuts-site latest bf9af7aee505 12 minutes ago 916MB

イメージは削除されていないので、コンテナを起動したい時はbuildをせずにupで起動することができます。この時コンテナ自体は新しく生成されて、前回のコンテナとは異なるコンテナとなります。

コンテナ起動中にdocker-compose.ymlを編集しない!

docker compose upによりコンテナを起動している間に定義ファイル(docker-compose.yml)を編集してしまうと、予期せぬバグが起きることがあります。

例えばコンテナ起動中にサービス名やコンテナ名を変更するとdocker compose downを行った時に、一致するコンテナが見つからず、コンテナを停止することができなくなってしまいます。
もしそうなってしまった場合にはdocker container stop 古いコンテナ名といったようにdocker composeコマンドを使わずに1つずつ実行することで、停止することができます。

基本的には docker-compose.yml はコンテナ起動中には触らないようにしたいですね。

開発環境向けに編集する

ここでdev.Dockerfiledocker-compose.dev.ymlが開発環境向けにできていないことが発覚しました。

開発環境では、編集がすぐに反映されなければいけません。

発生している問題の詳細

現在の設定では、コンテナ起動中(サイトが見れる状態のとき)に編集をしても編集は反映されません。再度ビルドをしてコンテナの生成をする必要があります。

# ビルドしてコンテナを起動する
$ docker compose -f docker-compose.dev.yml build
$ docker compose -f docker-compose.dev.yml up -d

http://localhost:3000 で確認します。

Next.js 部分のリンク色を変更してみます。

./style/Home.module.css
/* タイトルのリンク色を変更する */
.title a {
  color: green;
  text-decoration: none;
}

修正をして保存しても変更は反映されません。

また、コンテナの再起動だけでも変更は反映されません。

# コンテナを一度止める
$ docker compose -f docker-compose.dev.yml down

# 単にコンテナを起動し直しても反映されない
$ docker compose -f docker-compose.dev.yml up -d

再度ビルドをしてイメージを作り直すところから行います。

# コンテナを止める
$ docker compose -f docker-compose.dev.yml down

# ビルドしてコンテナを起動する
$ docker compose -f docker-compose.dev.yml build
$ docker compose -f docker-compose.dev.yml up -d

再ビルド + 再起動でようやく変更が反映されました。

これでは開発どころではありませんね 😠

Dockerfileを開発環境向けに書き換える

まずはDockerfileを開発環境向けに書き換えます。

前回の記事ではマルチステージビルドを行なって、より小さなイメージを作るような設計になっていました。

dev.Dockerfile(編集前)
# deps ステージ
FROM node:16 AS deps

WORKDIR /app

COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn install --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i; \
else echo "Lockfile not found." && exit 1; \
fi

# builder ステージ
FROM node:16 AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules

#### ここからが開発環境向けになっていない ####
COPY . .
RUN yarn build

# runner ステージ
FROM node:16 AS runner
WORKDIR /app

ENV NODE_ENV=development

COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json

COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

CMD ["node", "server.js"]

Docker を使わずに Next.js プロジェクトの開発を行う場合にも、依存ライブラリを追加した際には一度停止して、ライブラリのインストールを行う必要があります。

依存ライブラリが変更された場合には、コンテナを止めて再ビルドをすることになるので正しいと思うので、depsステージはこのままでよさそうです。

Next.js 公式の with-docker-composeサンプルdev.Dockerfileを参考にしました。

dev.Dockerfile
FROM node:16-alpine

WORKDIR /app

COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
  if [ -f yarn.lock ]; then yarn install --frozen-lockfile; \
  elif [ -f package-lock.json ]; then npm ci; \
  elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i; \
  else echo "Lockfile not found." && exit 1; \
  fi

COPY public ./public
COPY pages ./pages
COPY styles ./styles
COPY next.config.js .
COPY tsconfig.json .

CMD yarn dev

本番環境向けのビルドを行わずに、必要なファイルのコピーのみを行なっています。
マルチステージビルド自体行わない形になりました。

また、本番環境向けではないですが、node の通常版を利用してビルドするとイメージサイズが 2GB あることにびっくりしちゃって alpine(軽量版)に変更しました。公式サンプルも開発環境でも alpine を指定しているので大丈夫なはず。というか本番環境と揃えちゃうほうがいいのでは?って気もするので。(結局 以下で実行してみて、マルチステージビルドのすごさを実感することとなりました)

Docker Compose なしで動くか確認してみる

上記 Dockerfile でイメージを作成してコンテナが作成できるかどうかの確認をしてみました。

$ cd ~/NextProjects/donuts-site/

# docker build -f Dockerfile のファイル名 -t 作成するするイメージ名 もとになるDockerfileの場所
$ docker build -f dev.Dockerfile -t donut-site-image .

# image が作成できたかの確認
$ docker image ls

REPOSITORY                         TAG       IMAGE ID       CREATED          SIZE
donut-site-image                   latest    12055e36ff80   37 seconds ago   1.23GB

# コンテナの起動
$ docker run --name donut-site donut-site-image

http://localhost:3000 で表示ができたので、Dockerfile はよさそうです。

ボリュームのマウントをする

次に docker-compose.dev.ymlを編集します。
上記の Dockerfile を編集しただけでは、 コードの編集はコンテナを作り直すまで反映されません。

現在は Docker のコンテナ内にデータをコピーして、Docker コンテナ外のローカル上のデータを編集しているに過ぎません。
そのため、Docker コンテナで参照するデータをローカル上のデータに変更する必要があります。

volumes を追加します。

docker-compose.dev.yml
version: '3'

services:
  donuts-site:
    container_name: donuts-site
    build:
      context: .
      dockerfile: dev.Dockerfile
    ports:
      - 3000:3000
    volumes:
      - ./public:/app/src
      - ./pages:/app/pages
      - ./styles:/app/styles

これでdocker composeを実行してみます。

$ cd ~/NextProjects/donuts-site/

# ビルドしてコンテナを起動する
$ docker compose -f docker-compose.dev.yml build
$ docker compose -f docker-compose.dev.yml up -d

最後にスタイルを変更してみましょう。

./style/Home.module.css
/* タイトルのリンク色を変更する */
.title a {
  color: pink;
  text-decoration: none;
}

ホットリロードも効いていますね!素晴らしいです 😊

最終的な開発環境向けのファイル

ボリュームのマウントを利用しているので Dockerfile で実行している一部のCOPYコマンド部分必要ないと思うので削除しました。

dev.Dockerfile
FROM node:16-alpine

WORKDIR /app

COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
  if [ -f yarn.lock ]; then yarn install --frozen-lockfile; \
  elif [ -f package-lock.json ]; then npm ci; \
  elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i; \
  else echo "Lockfile not found." && exit 1; \
  fi

COPY next.config.js .
COPY tsconfig.json .

CMD yarn dev

Dockerfile の最後のCMD yarn devは yarn を利用していない老婆にはCMD npm run devでも動くことを確認しました。

docker-compose.dev.yml
version: '3'

services:
  donuts-site:
    container_name: donuts-site
    build:
      context: .
      dockerfile: dev.Dockerfile
    ports:
      - 3000:3000
    volumes:
      - ./public:/app/public
      - ./pages:/app/pages
      - ./styles:/app/styles
再ビルドが必要なケース

Dockerfile で COPYコマンドを行なっているファイルが編集された場合には、イメージの再ビルドとコンテナの再起動が必要になります。

具体的には、package.json yarn.lock* package-lock.json* pnpm-lock.yaml*next.config.jstsconfig.jsonのいずれかのファイルが編集された場合ですね。

おわりに

ようやく Next.js + Docker の開発環境の構築が完了しました。
手戻りが多く、今回のシリーズは実際に役に立てづらいと思いますので、まとめ版記事も用意したいところです。

次回は、本番環境向けの設定をしていこうと思います。