📦

Next.jsで学ぶDockerの基本

に公開

はじめに

本記事ではNext.jsアプリケーションの開発・本番環境をDockerで構築することを通じて,初学者向けにDockerの概要を紹介します.

早速ですが,Dockerの流れは以下の通りです.

  1. Dockerfileイメージ(コンテナの設計図)作成のためのスクリプトを記入する.
  2. Dcokerfileビルドしてイメージを作成する.
  3. 作成したイメージを用いてコンテナを作成・実行する.

作成するアプリケーションのソースコードは以下のGitHubリポジトリからご参照ください.

https://github.com/Go-Morishita/nextjs-container-test

本編

本編では,手順ベースで解説を進めていきます.毎回の手順が上で説明した流れのどの部分に当たるのかを意識しながら作業すると,簡単に理解することができます.

1. Next.jsアプリケーションの作成

まずnextjs-container-testという名前のNext.jsアプリケーションを作成します.

npx create-next-app@latest nextjs-container-test

2. 開発用のDockerfileの作成

開発用のコンテナイメージを作成するために必要なDockerfile.devを作成します.

Dockerfile.dev
FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install  

ENV HOSTNAME=0.0.0.0
EXPOSE 3000

CMD ["npm", "run", "dev", "--", "-p", "3000", "-H", "0.0.0.0"]
  • FROM node:20-alpine: ベースイメージを指定
  • WORKDIR /app: 作業ディレクトリを/appに設定
  • COPY package*.json ./: 依存関係ファイルのみをコピー
  • RUN npm install: 依存パッケージをインストール
  • ENV HOSTNAME=0.0.0.0: 環境変数HOSTNAMEの設定,後述の-H 0.0.0.0があるので不要.
  • EXPOSE 3000: コンテナが3000番ポートで待ち受ける「意図」を示す.
  • CMD ["npm", "run", "dev", "--", "-p", "3000", "-H", "0.0.0.0"]: コンテナ起動時のデフォルトコマンド,-p 3000で3000番ポートでサーバが待ち受けるようになり,-H 0.0.0.0で全てのネットワークからの接続を許可する.

3. 開発用コンテナイメージのビルド・実行

作成したDockerfile.devをビルドし,コンテナイメージを作成します.

docker build -f Dockerfile.dev -t next-dev .
  • -f: ビルドするDockerfileの指定
  • -t: イメージの名前の指定

コンテナイメージを用いてコンテナを作成・実行します.

docker run --rm -it -p 3000:3000 -v "$PWD":/app -v /app/node_modules next-dev
  • --rm: コンテナ終了時に自動で削除
  • -it: ログの閲覧や手動操作が可能
  • -p 3000:3000: ポートフォワーディングの設定,ホスト(左)の3000番ポートをコンテナ(右)の3000番ポートに繋ぐ,localhost:3000でアクセス可能になる.
  • -v "$PWD":/app: ボリュームマウント,現在の作業ディレクトリ"$PWD"をコンテナの内の作業ディレクトリ/appと同期させる.ホットリロードが可能になる.
  • -v /app/node_modules: 上記のボリュームマウントの際にホスト側の空のnode_modulsがコンテナ内のnode_modulesを上書きするのを防ぐための仕組み,匿名ボリュームという.
  • next-dev: 最後にイメージ名を指定

http://localhost:3000で立ち上がったらpage.tsxを編集し,リアルタイムで変更が反映されるか確認します.

4. 本番環境用のDockerfileの作成

本番環境用のコンテナイメージを作成するために必要なDockerfile.prodを作成します.

Dockerfile.prod
FROM node:20-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .  

RUN npm run build

ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
ENV PORT=3000
EXPOSE 3000

CMD ["npm", "run", "start", "--", "-p", "3000", "-H", "0.0.0.0"]
  • RUN npm ci: package-lock.jsonに厳密に一致するように依存関係をインストール
  • ENV NODE_ENV=production: 本番環境であることを明示的に指定(不要)
  • ENV PORT=3000: 3000番ポートを使用することを明示的に指定(不要)

5. 本番環境用コンテナイメージのビルド・実行

作成したDockerfile.prodをビルドし,コンテナイメージを作成します.

docker build -f Dockerfile.prod -t next-prod .

コンテナイメージを用いてコンテナを作成・実行します.

docker run --rm -p 3000:3000 next-prod

http://localhost:3000で問題なくアプリケーションが動作することを確認します.

番外編

番外編では少し難易度が上がります.ですが,Dockerを実運用する上で必須となる知識です.

1. 秘匿情報

コンテナ上で動くアプリケーション内で,以下のような.envファイル内の情報を取得します.

PASSWORD=aaaaiiiuuu1111

動作確認のために以下のような/app/api/env-check/route.tsを作成します.

route.ts
import { NextResponse } from "next/server";

export async function GET(req: Request) {
const password = process.env.PASSWORD;

if (!password) {
return NextResponse.json({ error: "PASSWORD not set" }, { status: 500 });
}

return NextResponse.json({ password });
}

Dockerfileやイメージビルドの変更は不要.実行時に.envを注入します.

docker build -f Dockerfile.prod -t next-prod .
docker run --rm -p 3000:3000 --env-file .env next-prod
  • --env-file: .envをコンテナ内に取り込む

以下のコマンドを実行して,秘密情報がアプリケーションで取得できているか確認します.

curl -s http://localhost:3000/api/env-check

2. Dockerfileの最適化

Dockerfileを最適化しないと、イメージが肥大化してビルドや配布が遅くなり、脆弱性や再現性の低下も招いて、結果的に運用コストと障害リスクが高まります。
以下の3つのポイントをまず押さえましょう.

  • コンテナレイヤー
  • マルチステージビルド
  • ディストロレス

コンテナレイヤー

コンテナレイヤーとは,Dockerイメージを構成する積み重ねられた層のことです.DockerはDockerfileの各命令(FROM RUN COPYなど)ごとにレイヤーを作り,それらをキャッシュとして再利用します.よって最適化では以下のような事が重要です.

  • 不要なレイヤーを作らずに,なるべく命令をまとめる.
  • 変更頻度が少ない命令を上に置いて,キャッシュ効率を上げる.

マルチステージビルド

マルチステージビルドとは,1つのDockerfile内で複数のFROM(=複数のステージ)を使い,ビルド用と実行用を分ける手法です.
アプリケーションをDockerイメージとして構築する際,ソースコードのビルドには多くの開発ツール(ビルドツール・コンパイラ)が必要です.しかし,ビルド後の実行環境にはそれらのツールは不要です.マルチステージビルドでは,この「ビルド時にだけ必要なもの」と「実行時にだけ必要なもの」を分離することで,最終イメージのサイズを大幅に削減したり,セキュリティリスクの低減にもつながります.

ディストロレス

ディストロレス(Distroless)とは,OSのディストリビューションを含まない実行専用の軽量イメージのことです.Dockerfileの最適化では,最終的なイメージサイズを小さくし,セキュリティリスクを減らす事が重要です.ディストロレスを使うことで,bashaptなどの不要なツールを完全に省き,アプリケーションの実行に必要な最低限のファイルだけを含めることができます.

本番環境用のDockerfileの最適化

本編で作成した本番環境用のDockerfile.prodを修正し,イメージを最適化してみましょう.前節までに説明した3つのポイントに従って実装を行います.

  • コンテナレイヤーの観点
    1. COPY package*.json ./RUN npm ciCOPY . .の順にすることでキャッシュを生かす.
    2. RUN npm run buildnpm prune --omit=devを一行のRUNにまとめ,不要なレイヤーを増やさないように工夫する.
    3. ENVを一行でまとめて定義することでレイヤーを節約する.
  • マルチステージビルドの観点
    1. FROM node:20-alpine AS builderでビルド専用ステージを作成する.
    2. 実行ステージにはビルド成果物(.nextpublicnode_modulespackage.json)のみをコピーする.
  • ディストロレスの観点
    1. 実行ステージをFROM gcr.io/distroless/nodejs20-debian12にして,不要なOSツールを省いた最小実行環境でセキュアかつ軽量にする.
    2. npm環境がないため,nodeから実行できるようにCMDを変更する.
    3. USER nonrootで非特権ユーザーでアプリケーションを実行することでセキュリティを強化する.

以下のものが最終完成版Dockerfile.prod.optimizeです.

Dockerfile.prod.optimize
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --no-audit --no-fund
COPY . .
RUN npm run build && npm prune --omit=dev

FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=builder /app/.next /app/.next
COPY --from=builder /app/public /app/public
COPY --from=builder /app/node_modules /app/node_modules
COPY --from=builder /app/package.json /app/package.json
ENV NODE_ENV=production HOSTNAME=0.0.0.0 PORT=3000
EXPOSE 3000
USER nonroot
CMD ["node_modules/next/dist/bin/next","start","-p","3000","-H","0.0.0.0"]

ビルド・実行を行い動作確認をしましょう.

docker build -f Dockerfile.prod.optimaze -t next-prod-optimize .
docker run --rm -p 3000:3000 --env-file .env next-prod-optimize

性能比較

最適化前のDockerfile.prodから作成したイメージであるnext-prodと最適化後のDockerfile.prod.optimizeから作成したイメージであるnext-prod-optimizeとの性能比較を以下の3つの観点から行います.

  • イメージサイズ
  • 脆弱性
  • 起動時間

まず,イメージサイズを調べます,以下のコマンドを使用しましょう.

docker images

元のイメージは1.86GBですが,最適化後は1.01GBになり大幅な削減が見られます.

REPOSITORY           TAG       IMAGE ID       CREATED      SIZE
next-prod-optimize   latest    d52b466887f5   3 days ago   1.01GB
next-prod            latest    f6f5c67d4cbe   7 days ago   1.86GB
next-dev             latest    b4b0296f3bd1   7 days ago   1.35GB

次に脆弱性を調べます.今回はTrivyというツールを使用します.以下のコマンドでインストールします.

brew install trivy

以下のコマンドを実行することで,イメージの脆弱性を診断できます.2つのイメージに対してそれぞれ実行しましょう.

trivy image <image-name>

出力がとても多いですが着目すべきは以下の部分です.元のイメージは全体としての脆弱性件数は8件と最適化後よりも下回っているものの,重要度の高い脆弱性が多く見られます.一方最適化後のイメージは12件の重要度の低い脆弱性のみになり,改善が確認できます.

next-prod
・・・
Total: 6 (UNKNOWN: 0, LOW: 2, MEDIUM: 4, HIGH: 0, CRITICAL: 0)
・・・
Total: 2 (UNKNOWN: 0, LOW: 1, MEDIUM: 0, HIGH: 1, CRITICAL: 0)
・・・
next-prod-optimize
・・・
Total: 12 (UNKNOWN: 0, LOW: 12, MEDIUM: 0, HIGH: 0, CRITICAL: 0)
・・・

最後に起動時間を調べます.2つのイメージをそれぞれ実行させましょう.

docker run --rm -p 3000:3000 --env-file .env <image-name>

元のイメージは起動時間が406msですが,最適化後は起動時間が204msと2倍の高速化が確認されました.

next-prod
   ▲ Next.js 15.5.4
   - Local:        http://localhost:3000
   - Network:      http://0.0.0.0:3000

 ✓ Starting...
 ✓ Ready in 406ms
next-prod-optimize
   ▲ Next.js 15.5.4
   - Local:        http://localhost:3000
   - Network:      http://0.0.0.0:3000

 ✓ Starting...
 ✓ Ready in 204ms

まとめ

本記事ではNext.jsアプリケーションを題材に,Dockerを用いた開発環境と本番環境の構築手順を解説しました.番外編では,セキュアな環境変数の扱い方やイメージの最適化について触れました.

初学者でも簡単にDockerの概要を理解できると思うので,ぜひ参考にしてみてください!


本記事は,日本仮想化技術株式会社でのインターン業務の一環として作成しております.ご興味のある方はいつでもご連絡ください.
https://virtualtech.jp/

Discussion