把一个 Next.js 项目塞进 Docker 容器,听起来只是写几行 Dockerfile 的事,但真正动手时问题会接连冒出来:构建阶段 node_modules 装不上、运行时找不到 .next 目录、环境变量没注入导致页面报错、镜像体积逼近 2GB 让 CI 流水线慢得离谱。这篇文章把容器化 Next.js 的完整链路拆开讲清楚,从输出模式的选择到 Dockerfile 的多阶段构建,再到生产环境的健康检查与反向代理配合,看完你就能写出一份可直接投产的配置。

一、先搞清楚输出模式:standalone 才是容器化的正确姿势
Next.js 从 12.x 开始提供了 output: 'standalone' 配置,这是容器化部署的基础。开启之后,执行 next build 会在 .next/standalone 目录生成一个极简的运行环境:只包含必要的 server.js、精简后的 node_modules 和静态资源引用,不再需要把完整的依赖树搬进镜像。
很多教程忽略了一个细节:standalone 模式生成的目录里,.next/static 和 public 并不会自动包含在 standalone 内部,需要手动复制过去。漏掉这一步的典型症状是页面能打开但样式全丢、图片 404,因为静态资源请求落到了不存在的路径上。
在 next.config.js 中的配置非常简单:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
// 生产环境建议关闭遥测,减少构建日志噪音
experimental: {
telemetry: false,
},
};
module.exports = nextConfig;需要额外说明的是,如果你的项目使用了 App Router 并且依赖了某些只在服务端加载的原生模块(比如 sharp、bcrypt),要确认这些依赖被正确打包进 standalone 输出。Next.js 会自动追踪 import 关系,但通过 require 动态拼接的路径它无法分析,此时需要用 outputFileTracingIncludes 显式声明。
二、编写生产级 Dockerfile:多阶段构建与缓存优化
一个糟糕的 Dockerfile 往往长这样:单阶段构建,先 COPY . . 再 npm install,结果每改一行代码都要重装全部依赖。正确的做法是利用 Docker 的层缓存机制:先只复制 package.json 和锁文件安装依赖,再复制源码执行构建。只要依赖不变,缓存就能命中,构建时间从几分钟降到十几秒。
下面这份 Dockerfile 经过实际项目验证,最终镜像体积约 130MB,可以直接拿去改用:
# ---------- 第一阶段:安装依赖 ---------- FROM node:20-alpine AS deps WORKDIR /app # 只复制依赖声明,最大化利用缓存 COPY package.json package-lock.json ./ RUN npm ci # ---------- 第二阶段:构建 ---------- FROM node:20-alpine AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . ENV NEXT_TELEMETRY_DISABLED=1 RUN npm run build # ---------- 第三阶段:运行 ---------- FROM node:20-alpine AS runner WORKDIR /app ENV NODE_ENV=production # 创建非 root 用户运行,提升安全性 RUN addgroup -S nodejs && adduser -S nextjs -G nodejs # standalone 模式下手动复制静态资源 COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/public ./public USER nextjs EXPOSE 3000 ENV PORT=3000 HOSTNAME=0.0.0.0 CMD ["node", "server.js"]
这份配置里有三个容易被忽略的要点。第一,HOSTNAME=0.0.0.0 必须设置,否则容器内服务只监听 localhost,从宿主机访问会直接连接被拒绝。第二,运行阶段使用非 root 用户是生产环境的基本规范,避免容器被攻破后拿到宿主机权限。第三,alpine 基础镜像虽然小,但如果项目依赖了 glibc 编译的原生扩展(例如某些数据库驱动),需要换成 debian-slim 版础镜像,否则运行时会报找不到共享库的错误。
另外别忘了在项目根目录添加 .dockerignore,把 node_modules、.next、.git 排除掉,否则本地几 GB 的缓存目录会被一起发送给 Docker 守护进程,拖慢构建还在某些场景下造成覆盖冲突。
三、环境变量与运行时配置的坑
Next.js 的环境变量分为构建时和运行时两类,这是容器化时最容易踩的坑。NEXT_PUBLIC_ 前缀的变量会在构建时被内联到客户端 JS 里,这意味着它们必须在 docker build 阶段通过 --build-arg 传入,部署后再修改环境变量是完全无效的——浏览器里跑的还是构建时写死的值。
而服务端使用的变量(比如数据库连接串、API 密钥)则应该在运行时注入,通过 docker run -e 或编排文件的环境变量配置传入。一个常见的分层策略是:客户端需要的公共配置(如站点地址)走构建参数,敏感信息和服务端配置走运行时环境变量,这样同一份镜像可以在测试、预发、生产环境之间复用,只切换环境变量即可。
如果确实需要运行时读取动态的客户端配置,可以借助一个简单的方案:让页面在服务端组件中读取 process.env 再作为 props 传给客户端组件,变量就在每次请求时生效了,绕开了构建时内联的限制。
四、健康检查、反向代理与上线检查清单
容器编排平台(Kubernetes、Docker Compose)依赖健康检查来判断实例状态。Next.js 本身没有提供健康检查端点,最简单的做法是在 app/api/health/route.js 里加一个返回 200 的接口,然后在 Dockerfile 或编排配置中声明检查:
HEALTHCHECK --interval=30s --timeout=3s --start-period=15s \ CMD wget -qO- http://127.0.0.1:3000/api/health || exit 1
生产环境通常不会让容器直接暴露公网,前面还会挂一层 Nginx。这里要注意代理转发时的几个 header:X-Forwarded-Proto 必须传给容器,否则 Next.js 中间件里基于 request.nextUrl.protocol 的判断会拿到 http,导致重定向循环。Nginx 的关键配置如下:
location / {
proxy_pass http://app:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}最后列一份上线前的检查清单:确认镜像内监听 0.0.0.0 而非 localhost;验证静态资源路径可访问;检查 NEXT_PUBLIC_ 变量的值是否为构建时的预期值;确认健康检查端点返回正常;用 docker stats 观察容器内存占用是否稳定(Node 默认堆内存上限在容器里可能需要通过 NODE_OPTIONS=--max-old-space-size=512 显式限制)。把这些点逐项过一遍,你的 Next.js 容器化部署就算真正落地了。