针对 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 镜像,仅包含独立部署输出。 原因:源代码变更仅重建最后两个阶段,且 runner 无 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-version中的 Node 版本,且没有用于 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,从发布的资产摘要中获取 sha256 值(gh release view v<version> --repo pnpm/pnpm --json assets),并在同一提交中升级packageManager。如果发布格式发生变化,备用方案:将pruner/deps/builder阶段基于ghcr.io/pnpm/pnpm:<version>(无 Node,需添加),而不是发明新的安装方法。 -
runner 阶段仅复制三样内容。
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;缺少它们应用将无法提供 CSS 或 JS。在 monorepo 中,服务器位于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> -
在 runner 阶段设置
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 会忽略该设置,并使用 chart 中同一路径的探针。 -
构建参数仅用于构建时内联的值:
NEXT_PUBLIC_*和NEXT_DEPLOYMENT_ID。NEXT_PUBLIC_*会被编译到 JS 中,因此必须在builder阶段作为ARG+ENV存在,并且在镜像运行的每个环境中都是相同的。每个环境特有的内容通过 chart 中的运行时环境变量提供。如需添加一个: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。不要硬编码版本标签。 -
保持各阶段无应用特定的 hack。 不在 runner 中执行
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