イメージサイズ削減の重要性
Next.jsアプリケーションを開発する際、コントロールを怠るとイメージサイズの増加が致命的なパフォーマンス低下やデプロイメントの障害に直結します。
特に、クラウドプラットフォームを使用している場合、イメージサイズが肥大化すると、デプロイメントやスケーリングの速度が著しく低下し、予想外のコスト増加を招く可能性が高まります。
この記事では、Next.jsアプリケーションのイメージサイズを効果的に削減し、これらのリスクを回避するためのマルチステージビルド手法に焦点を当てます。
Dockerを駆使してビルドプロセスを最適化し、不要なファイルや依存関係を徹底的に排除することで、イメージサイズを大幅に削減し、クラウド環境でのアプリケーションのデプロイメントやパフォーマンスを大きく向上させることが可能です。
結果
1.91GB
から162MB
になりました。
REPOSITORY SIZE
single-stage-build 1.91GB
multi-stage-build 162MB
変更前
FROM node:18-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm install
COPY . .
RUN npx prisma generate
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
公式サンプル
こちらを参考にしました。
https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile
standaloneモード
standaloneとは?
https://nextjs.org/docs/app/api-reference/next-config-js/output#automatically-copying-traced-files
Next.jsが自動的に必要なファイルだけをコピーするスタンドアロンのフォルダを作成できます。
これにより、node_modulesのインストールなしでデプロイできるようになります。さらに、最小限のserver.jsファイルも出力され、これはnext startの代わりに使用できます。
ただし、この最小限のサーバーはデフォルトではpublicフォルダや.next/staticフォルダをコピーしないため、これらのフォルダは手動でstandalone/publicやstandalone/.next/staticフォルダにコピーする必要があります。その後、server.jsファイルはこれらを自動的に提供します。
// next.config.js
module.exports = {
// ... rest of the configuration.
output: "standalone",
};
変更後
# base ステージ
FROM node:18-alpine AS base
# deps ステージ
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package*.json ./
RUN npm ci
# builder ステージ
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npx prisma generate
RUN npm run build
# runner ステージ
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
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
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 HOSTNAME="0.0.0.0" node server.js
各ステージの説明と目的
base ステージ
- 目的: 全てのステージで共通の基盤となるイメージを提供します
- 役割: node:18-alpineをベースイメージとして使用し、これにより他のステージでNode.js環境が利用可能になります
deps ステージ
- 目的: アプリケーションの依存関係をインストールします
- 役割: libc6-compatパッケージをインストールし、package.jsonとpackage-lock.jsonをコピーしてから、npm ciコマンドを実行して依存関係をインストールします。これにより、後続のステージで依存関係を再利用できます。libc6-compatパッケージのインストールは、Alpine Linux上でglibcに依存するバイナリが正しく動作するようにするために必要です。これは、特定のNode.jsパッケージや依存関係がglibcに依存している場合に重要となります。
builder ステージ
- 目的: アプリケーションのビルドを行います。
- 役割: depsステージでインストールされたnode_modulesをコピーし、アプリケーションのソースコードをコピーします。その後、Prismaクライアントを生成し(npx prisma generate)、アプリケーションをビルドします(npm run build)。このステージでは、ビルドプロセスに必要な全ての操作が行われます。
runner ステージ
- 目的: ビルドされたアプリケーションを実行環境に準備します
- 役割: 専用のユーザー(nextjs)とグループ(nodejs)を作成し、builderステージでビルドされたアプリケーションのファイルを適切な場所にコピーします。最後に、nextjsユーザーでアプリケーションを実行します。
このマルチステージビルドプロセスにより、ビルド時と実行時の依存関係を明確に分離し、最終的なイメージのサイズを小さく保つことができます。また、セキュリティリスクを最小限に抑えることができます。
まとめ
信じられないことに、イメージのサイズが1.91GBからわずか162MBにまで削減されました!
これは、なんとサイズが10分の1以下になったことを意味します!
1.91GBは約1,910MBですが、それがたったの162MBにまで削減されたのですから、なんと約11.8倍ものサイズ削減が達成されたことになります!
まさに驚異的な最適化が達成され、サイズが驚くほど大幅に削減されたことが証明されました!