Next.jsアプリケーションのイメージサイズを劇的に削減するマルチステージビルドの魔法

# イメージサイズ削減の重要性
Next.jsアプリケーションを開発する際、コントロールを怠るとイメージサイズの増加が致命的なパフォーマンス低下やデプロイメントの障害に直結します。  
特に、クラウドプラットフォームを使用している場合、イメージサイズが肥大化すると、デプロイメントやスケーリングの速度が著しく低下し、予想外のコスト増加を招く可能性が高まります。  
この記事では、Next.jsアプリケーションのイメージサイズを効果的に削減し、これらのリスクを回避するためのマルチステージビルド手法に焦点を当てます。  
Dockerを駆使してビルドプロセスを最適化し、不要なファイルや依存関係を徹底的に排除することで、イメージサイズを大幅に削減し、クラウド環境でのアプリケーションのデプロイメントやパフォーマンスを大きく向上させることが可能です。

# 結果
`1.91GB`から`162MB`になりました。
```bash
REPOSITORY SIZE
single-stage-build 1.91GB
multi-stage-build 162MB
```

## 変更前
```Dockerfile
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ファイルはこれらを自動的に提供します。  
```js
// next.config.js
module.exports = {
  // ... rest of the configuration.
  output: "standalone",
};
```

## 変更後
```Dockerfile
# 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倍ものサイズ削減が達成されたことになります！  
まさに驚異的な最適化が達成され、サイズが驚くほど大幅に削減されたことが証明されました！