Dockerize Next.js Standalone: Imageサイズを1.2GBから120MBへ劇的に削減

Docker tutorial - IT technology blog
Docker tutorial - IT technology blog

課題:肥大化するNext.jsのDocker Image

Next.jsを従来の方式でVPSにデプロイした際、Imageのサイズが数GBにもなり、驚いた経験はありませんか?主な原因は、実行環境にnode_modules(dev用とprod用の両方)を丸ごと含めてしまっていることにあります。その結果、RegistryへのPush/Pullに5〜7分もかかり、帯域を浪費し、CI/CDサイクル全体を遅延させてしまいます。

なぜシンプルなWebアプリケーションが、デスクトップソフトウェアのように重くなってしまうのでしょうか?その答えは、不要な「重い荷物」にあります。Next.jsはバージョン12.2から、アプリケーションの実行に必要な最小限のファイルのみを抽出するStandalone Modeをサポートしています。この機能を適用することで、私はImageサイズを1.2GBからわずか120MBにまで削減することができました。

Standalone Mode:その仕組みとは?

通常、next startコマンドを実行するには依存関係ディレクトリ全体が必要です。しかし実際には、Production環境での動作にビルド用やテスト用のパッケージは不要です。

standaloneを有効にすると、Next.jsは@vercel/nftを使用してソースコードをインテリジェントに分析します。必要なファイルだけを自動的にピックアップし、.next/standaloneディレクトリにまとめます。このディレクトリには、元のディレクトリがなくてもアプリが自立して動作できる、最適化されたnode_modulesが含まれています。

これがMulti-stage buildsを実現する鍵となります。あるステージでアプリをビルドし、最終ステージ(runner)にはこの軽量なディレクトリのみをコピーします。これにより、最終的なImageは非常にコンパクトになります。

最適化されたImage作成のステップ

ステップ1:Next.jsの設定

まず、next.config.jsでこの機能を有効にします:

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
}

module.exports = nextConfig

npm run buildを実行すると、.next/standaloneディレクトリが生成されます。これがDockerに組み込むアプリケーションの「心臓部」となります。

ステップ2:.dockerignoreの活用

不要なファイルをスキャンしてビルド時間を無駄にしないよう、このファイルを忘れないでください。適切に設定された.dockerignoreは、ビルド速度を劇的に向上させます。

node_modules
.next
.git
.env*
README.md

ステップ3:最適化されたDockerfile(Multi-stage)

以下は、depsbuilderrunnerの3ステージ構成のDockerfileです。このように分割することで、Dockerのレイヤーキャッシュを最大限に活用できます。

# ステージ1:依存関係のインストール
FROM node:20-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app

COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
  if [ -f package-lock.json ]; then npm ci; \
  elif [ -f yarn.lock ]; then yarn --frozen-lockfile; \
  elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
  else echo "Lockfileが見つかりません。" && exit 1; \
  fi

# ステージ2:アプリケーションのビルド
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

ENV NEXT_TELEMETRY_DISABLED 1
RUN npm run build

# ステージ3:Runner - 本番環境
FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

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

# 権限の設定
RUN mkdir .next
RUN chown nextjs:nodejs .next

# standaloneモードの出力をコピー
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ 
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000
ENV PORT 3000

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

なぜステージ3が効果的なのか?

  • node:20-alpineを使用することで、ベースImageを最小限に抑えています。
  • npm経由ではなく、node server.jsを直接実行することで、リソースのオーバーヘッドを削減します。
  • standaloneモードでは静的ファイルや公開ファイルが自動的に集約されないため、staticpublicを手動でコピーする必要があります。

ステップ4:結果の確認

ビルドしてImageサイズを確認しましょう:

docker build -t nextjs-app .
docker images

違いは歴然です。以前は1.2GBあったImageが、今では120MB〜150MB程度に収まっているはずです。私の環境では、GitHub Actionsでの実際のデプロイ時間が4分から45秒に短縮されました。

現場で得た「実践的な知恵」

1. 環境変数 (Environment Variables): Standaloneモードでは、ビルド時に環境変数が固定されます。柔軟性を持たせるには、コード内でprocess.envを使用し、docker run時に-eフラグで値を渡すようにします。

2. 設定デバッグのコツ: Dockerの設定エラー時、ターミナルの生のJSONログを読むのは苦痛です。私は、toolcraft.app/ja/tools/developer/json-formatter のフォーマッタにログを素早く貼り付けて確認しています。VS Codeに拡張機能を入れるよりもずっと手軽です。 これにより、開発環境と本番環境の差異をすぐに見つけることができます。

3. Sharpライブラリを忘れずに: アプリでnext/imageを使用している場合は、builderステージでsharpをインストールしてください。これがないと、Next.jsの画像最適化機能が非常に低速になり、サーバーのCPUを浪費します。

結論

Dockerの最適化は、単にストレージを数GB節約するためだけのものではありません。ビルドからデプロイまでのプロセス全体をスムーズかつプロフェッショナルなものに変えてくれます。Standaloneモードを使えば、重いImageのプル中にメモリ不足でサーバーがフリーズする心配もなくなります。

このテクニックがデプロイ時間の短縮に役立ち、より重要な作業に時間を割けるようになることを願っています。設定中にエラーが発生した場合は、お気軽にコメント欄で質問してください!

Share: