Next.js 適用的 Docker
用於應用程式映像檔。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> .
規則
-
五個階段:
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 映像檔,僅包含獨立輸出。 原因:原始碼變更只會重建最後兩個階段,且執行環境中沒有 pnpm、原始碼或開發依賴項。
-
將 Node 映像檔鎖定到確切版本與摘要,並與
.node-version保持一致。 在一個ARG NODE_IMAGE中使用node:26.12.0-alpine3.24@sha256:6f08…,同時用於base和runner。此摘要是多架構索引,因此 amd64 和 arm64 皆可解析。 為什麼選擇 Alpine 而非 distroless:distroless Node 僅發布浮動標籤(latest、nonroot),因此無法將確切的 Node 版本與.node-version匹配,且它沒有用於 HEALTHCHECK 的wget。Alpine 體積小,可精確鎖定,且sharp提供預建置的 musl 二進位檔,因此映像檔最佳化無需額外套件。 -
從 GitHub 發行版二進位檔安裝 pnpm,並經由 SHA-256 驗證。絕不使用
npm i -g pnpm、Corepack 或未經驗證的curl | sh。 一個ARG PNPM_VERSION等於packageManager,再加上每個架構一個用於pnpm-linux-{x64,arm64}-musl.tar.gz的校驗和,透過TARGETARCH選擇。更新時:變更三個 ARG,從發行版的資產摘要(gh release view v<version> --repo pnpm/pnpm --json assets)取得 sha256 值,並在同一次提交中更新packageManager。如果發行版格式變更,備案是將pruner/deps/builder階段基於ghcr.io/pnpm/pnpm:<version>(不含 Node,需自行添加),而不是發明新的安裝方法。 -
執行環境僅複製三項內容。
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使用 busybox 的wget存取/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(應用程式不需要 shell 工具集),不要有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