Docker for Next.js
アプリイメージに使用します。Next.js のスタンドアロンに関する詳細は nextjs-production(references/self-hosting.md)にあります。Kubernetes でイメージを実行する方法は helm-nextjs、CI でのビルドとプッシュ方法は github-actions を参照してください。
テンプレートの apps/<app>/Dockerfile が信頼できる情報源です。references/Dockerfile.md に完全版と注釈があります。リポジトリルートからビルドします:docker build -f apps/<app>/Dockerfile -t <app> .
ルール
-
5 ステージ構成:
base→pruner→deps→builder→runner。base:Node + pnpm、それ以外はなし。pruner:リポジトリ全体に対してturbo prune <app> --dockerを実行し、アプリとそのワークスペース依存関係のみを残します。deps:out/json/(マニフェスト)とout/pnpm-lock.yamlのみをコピーし、pnpm fetch+pnpm install --frozen-lockfile --offlineを実行します。このレイヤーはマニフェストまたはロックファイルが変更されるまで再利用されるため、ソースコードの編集では再インストールが発生しません。builder:out/full/(ソース)をコピーし、pnpm exec turbo run build --filter=<app>を実行します。runner:スタンドアロン出力のみを含む新しい Node イメージ。 理由:ソースコードの変更は最後の 2 ステージのみを再ビルドし、ランナーには pnpm、ソース、開発依存関係が含まれません。
-
Node イメージを正確なバージョンとダイジェストで固定し、
.node-versionに一致させる。node:26.12.0-alpine3.24@sha256:6f08…を 1 つのARG NODE_IMAGEで指定し、baseとrunnerの両方で使用します。ダイジェストはマルチアーキテクチャインデックスであるため、amd64 と arm64 の両方で解決されます。 理由:Alpine を distroless よりも優先する理由:distroless Node はフローティングタグ(latest、nonroot)のみを公開するため、正確な Node バージョンを.node-versionに一致させることができず、HEALTHCHECK 用のwgetもありません。Alpine は小型で、正確に固定でき、sharpはプリビルドされた musl バイナリを提供するため、追加パッケージなしで画像最適化が機能します。 -
pnpm を GitHub リリースのバイナリからインストールし、SHA-256 で検証する。
npm i -g pnpm、Corepack、未検証のcurl | shは絶対に使用しない。 1 つのARG PNPM_VERSIONをpackageManagerと等しく設定し、アーキテクチャごとにpnpm-linux-{x64,arm64}-musl.tar.gzのチェックサムを 1 つ用意し、TARGETARCHで選択します。バンプするには:3 つの ARG を変更し、sha256 値をリリースのアセットダイジェスト(gh release view v<version> --repo pnpm/pnpm --json assets)から取得し、同じコミットでpackageManagerをバンプします。リリース形式が変更された場合のフォールバック:pruner/deps/builderステージをghcr.io/pnpm/pnpm:<version>ベースにし(Node なし、追加する)、新しいインストール方法を考案するのではなく、この方法を採用します。 -
ランナーは正確に 3 つの要素をコピーする。
COPY --from=builder /repo/apps/<app>/.next/standalone ./ COPY --from=builder /repo/apps/<app>/.next/static ./apps/<app>/.next/static COPY --from=builder /repo/apps/<app>/public ./apps/<app>/publicスタンドアロンには
staticやpublicは含まれません。それらがないと、アプリは JS や CSS を提供できません。モノレポでは、サーバーはapps/<app>/server.jsです。CMD ["node", "apps/<app>/server.js"]、exec 形式であるため、Node は直接 SIGTERM を受け取ります。 -
uid/gid 1001 で実行し、ファイルは root が所有する。
nextjs:nodejs(1001:1001)を作成し、コピーしたファイルは root 所有のままにし(アプリは読み取り可能だが、自身のコードを変更できない)、apps/<app>/.next/cacheのみchownします。USER 1001:1001、数値形式で、Kubernetes が名前解決なしでrunAsNonRootを検証できるようにします。 -
イメージは読み取り専用のルートファイルシステムで動作する必要がある。 唯一のランタイム書き込みは
.next/cache(画像最適化、ISR/フェッチキャッシュ)に行われます。Kubernetes はそこにemptyDirをマウントします。ローカルでは--tmpfsを使用します。テスト:docker run --rm -p 3000:3000 --read-only \ --tmpfs /app/apps/<app>/.next/cache:uid=1001,gid=1001 \ --cap-drop ALL --security-opt no-new-privileges <app> -
ランナーで
NODE_ENV=production、HOSTNAME=0.0.0.0、PORT=3000、NEXT_TELEMETRY_DISABLED=1を設定する。HOSTNAME=0.0.0.0がないと、スタンドアロンサーバーはコンテナホスト名にバインドされ、プローブが失敗します。 -
HEALTHCHECKは busyboxwgetで/api/healthをチェックする。docker run/Compose 用です。Kubernetes はこれを無視し、同じパスでチャートのプローブを使用します。 -
ビルド引数はビルド時にインライン化される値のみ:
NEXT_PUBLIC_*とNEXT_DEPLOYMENT_ID。NEXT_PUBLIC_*は JS にコンパイルされるため、builderでARG+ENVにする必要があり、イメージが実行されるすべての環境で同一です。環境ごとの設定はすべて、チャートからのランタイム環境変数です。追加する場合:ARG NEXT_PUBLIC_APP_NAME ENV NEXT_PUBLIC_APP_NAME=${NEXT_PUBLIC_APP_NAME}…さらに、両方のワークフローで
build-argsを指定し、NEXT_PUBLIC_*はすでに turbo のenvに含まれています。 -
**シークレットは決して
ARG、ENV、COPYに入れない。**イメージ履歴に残ります。BuildKit のシークレットマウントを使用し、そのRUNの間のみ存在します:RUN --mount=type=secret,id=npm_token,env=NPM_TOKEN pnpm fetch # プライベートレジストリテンプレートのビルドステップは、オプションの
next_server_actions_keyシークレットをこの方法で既に宣言しています。 -
BuildKit キャッシュマウントで pnpm ストアをキャッシュする。
fetch/install/dlxで--mount=type=cache,id=pnpm-store,target=/pnpm/storeを使用し、pnpm_config_store_dir=/pnpm/storeを設定します。ストアは、同一ビルダー上のビルド間で存続し、レイヤーに含まれません。CI では、レイヤーキャッシュはtype=ghaです(github-actionsを参照)。 -
リポジトリルートの
.dockerignoreでコンテキストを小さく保ち、シークレットを除外する。node_modules、.next、.turbo、dist、coverage、.env*、.git、.github、chartsを除外します。Windows や macOS からnode_modulesを含むコンテキストは、Linux イメージ内のネイティブパッケージを破損させます。 -
**マルチアーキテクチャは
linux/amd64,linux/arm64で、CI によってビルドされる。**ローカルでは、自分のアーキテクチャ用にビルドします。すべてのステージはアーキテクチャに依存しませんが、pnpm ダウンロードはTARGETARCHからチェックサムを選択します。 -
ラベル: Dockerfile は
org.opencontainers.image.titleとlicensesを設定します。CI はdocker/metadata-actionを介してsource、revision、version、createdを追加します。バージョンラベルをハードコードしないでください。 -
**ステージをアプリ固有のハックから解放する。**ランナーで
apk addは行わず(アプリはシェルツールセットを必要としません)、npmも、ホストからコピーしたnode_modulesも使用しません。アプリがランタイムにネイティブツールを必要とする場合は、コメントで正当化してください。
デバッグ
| 症状 | 原因 |
|---|---|
ビルド中に extends: … doesn't resolve | アプリに必要なルートレベルのファイルがプルーンツリーに含まれていなかった:ワークスペースパッケージに移動する |
deps で ERR_PNPM_OUTDATED_LOCKFILE | マニフェスト変更後にロックファイルがコミットされていない |
ERR_PNPM_IGNORED_BUILDS | 新しい依存関係がインストールスクリプトを必要としている:allowBuilds で決定する |
| アプリが起動するが、CSS/JS がない | .next/static または public が apps/<app>/ にコピーされていない |
| ヘルスチェックが失敗するが、ログは正常 | HOSTNAME が 0.0.0.0 ではない |
ランタイムに EROFS | 何かが .next/cache 以外に書き込んでいる。特定し、--read-only を削除しない |
NEXT_PUBLIC_X が空 | ビルド引数として渡されていない(ビルド時に組み込まれる) |
確認
docker build -f apps/<app>/Dockerfile -t <app>:local --build-arg NEXT_DEPLOYMENT_ID=local .
docker run -d --name <app> -p 3000:3000 --read-only --tmpfs /app/apps/<app>/.next/cache:uid=1001,gid=1001 --cap-drop ALL <app>:local
curl -fsS localhost:3000/api/health
docker exec <app> id # uid=1001(nextjs)
docker inspect --format '{{.State.Health.Status}}' <app> # ~30秒後に healthy
docker rm -f <app>
ソース
- https://nextjs.org/docs/app/api-reference/config/next-config-js/output
- https://nextjs.org/docs/app/guides/self-hosting
- https://turborepo.dev/docs/guides/tools/docker
- https://pnpm.io/docker
- https://pnpm.io/cli/fetch
- https://github.com/pnpm/pnpm/releases
- https://docs.docker.com/build/building/best-practices/
- https://docs.docker.com/build/building/secrets/
- https://docs.docker.com/build/cache/optimize/#use-cache-mounts
- https://docs.docker.com/reference/dockerfile/#healthcheck
- https://hub.docker.com/_/node
- https://github.com/opencontainers/image-spec/blob/main/annotations.md