🐳

モノレポ構成のNext.jsプロジェクトをDocker化する方法

2024/12/22に公開

はじめに

モノレポ構成の Next.js アプリを Docker 化する方法の紹介です。Webアプリの開発でモノレポを導入することはだいぶ一般的になってきましたが、Docker化するための情報があまりなくハマりポイントが多かったのでポイントを交えて紹介します。おなじような悩みを抱えている方のお役にたてれば幸いです。

準備

ビルドに使うライブラリ

pNPM を使ってモノレポプロジェクトを管理して Turborepo でビルドを実行します。今回利用した各種ライブラリのバージョンは以下です。

  • pnpm 9.15.1
  • Turborepo 2.3.3
  • Nextjs 14.2.8
  • React 18.3.1

ポイントは3つ

モノレポ構成の Next.js アプリを Docker化するためのポイントは以下の3つです。

  • Dockerfileはアプリごとに配置する
  • Next.jsアプリをStandaloneビルドする
  • Turborepoのpruneを使って必要なファイルのみコンテナ化する

それではモノレポのプロジェクト構成、Next.jsのビルド設定、Dockerfileの内容の順に説明していきます。

モノレポのプロジェクト構成

ビルドする Next.js アプリのパッケージ構成は以下のようにします。

root
 ├─ package.json
 ├─ pnpm-workspace.yaml
 ├─ turbo.json
 ├─ apps/
 |   └─ web
 |       ├─ package.json
 |       └─ Dockerfile
 └─ packages/
     ├─ component
     |   └─ package.json
     └─ tsconfig
         └─ package.json

プロジェクトは component と tsconfig に依存する web という名前の Next.js アプリで構成されています。Dockerfile は web の下に配置しているのがポイントです。ちなみに Docker コマンド(ビルドなど)はルートディレクトリで実行します。

Next.js のビルド設定

Next.js アプリのビルドですが通常のビルドの場合、ビルド対象以外の余計なファイル(node_modulesなど)が含まれイメージサイズが大きくなるので Standalone ビルドすることをおすすめします。Next.js アプリを Standalone ビルドするには next.config.mjs を以下のように設定します。

next.config.mjs
import path from 'node:path'
import { fileURLToPath } from 'url'

const __dirname = path.dirname(fileURLToPath(import.meta.url))

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    // モノレポ構成のStandaloneビルド対策
    outputFileTracingRoot: path.join(__dirname, '../../')
  },
  output: 'standalone'
}
export default nextConfig

outputFileTracingRoot の設定に気づかず server.js の起動ができなくて4-5時間ハマりました。

Dockerfile の内容

ここまでの設定でやっと Next.js のビルドができるようになりました。Dockerfile の内容は以下になります。turbo prune でビルドに必要なファイルのみコンテナ化するのがポイントです。

apps/web/Dockerfile
FROM node:20-slim AS base

ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"

# pnpmを有効化
RUN corepack enable
RUN pnpm add -g turbo@^2

FROM base AS extractor
WORKDIR /app

# プロジェクトファイルを雑に全部コピーする
COPY . .

# ビルドに必要なファイルのみ抽出する
# ビルドに必要なファイルはルート直下のoutフォルダに格納される
RUN turbo prune web --docker

FROM base AS builder
WORKDIR /app

# turbo pruneで抽出したファイルだけコピーしてビルドまで実施する
COPY --from=extractor /app/out/json/ .
COPY --from=extractor /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
COPY --from=extractor /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml
RUN pnpm install --frozen-lockfile

COPY --from=extractor /app/out/full/ .
RUN pnpm run build --filter=web

FROM base AS runner
WORKDIR /app

# rootユーザで実行しない
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
USER nextjs

COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public

EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"

CMD [ "node", "apps/web/server.js" ]

Next.js の Standalone ビルドと Turborepo の prune の効果でイメージのサイズはだいぶ抑えられています。

Dockerfileのビルド

Dockerfileのビルドはプロジェクトのルートディレクトリで実行します。

$ docker build --platform linux/amd64 -f apps/web/Dockerfile -t ${{イメージ名}}:latest .

ローカルで起動する場合

プロジェクトルートに以下のような compose.yaml を配置して、実行すればOKです。

compose.yaml
services:
  app:
    container_name: "web"
    build:
      context: .
      dockerfile: ./apps/web/Dockerfile
    ports:
      - "3000:3000"

参考記事

Discussion