跳到主要内容

把 Next.js 14+ App Router 应用部署到 CloudBase 云托管

一句话定义:用 next.config.jsoutput: 'standalone' 生成精简产物,写一个多阶段 Dockerfile,通过 tcb cloudrun deploy 把 Next.js App Router 应用整体跑在 CloudBase 云托管上,含 SSR、流式、环境变量、自定义域名。

预计耗时:30 分钟 | 难度:进阶

适用场景

这一篇覆盖的场景:用 Next.js 14+ 构建应用,需要镜像在境内、域名可备案、和其他云资源(数据库、对象存储、云函数)内网互通。CloudBase 云托管完整支持 Next.js 的 SSR、流式响应和 Server Actions。

  • 适用:Next.js 14 / 15 + App Router + SSR / Streaming / Server Actions
  • 适用:希望用同一个仓库、同一个部署流程管理前端 + 后端 API
  • 不适用:纯静态站点(SSG),用 CloudBase 静态网站托管 更适合
  • 不适用:完全无服务端逻辑的演示项目,挂 GitHub Pages 即可

云托管和云函数的区别一句话:云函数适合"短任务、按请求计费、冷启动可接受",云托管适合"长驻进程、复杂运行时、有 WebSocket / SSE 长连接需求"。Next.js 这种带完整 SSR 生命周期的应用,通常都走云托管

环境要求

依赖版本
Node.js(本地开发 + 镜像内运行时)≥ 18.18(Next.js 14 最低要求)
Next.js14.x 或 15.x
Docker(本地构建验证)latest
@cloudbase/clilatest
一个已开通的 CloudBase 环境含云托管能力

第一步:开启 standalone 输出

next.config.js 加一行:

/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
// 其他配置...
};

module.exports = nextConfig;

output: 'standalone' 会让 next build.next/standalone/ 下生成一份"自包含"的运行产物:包含一个精简的 node_modules(只含运行时实际用到的依赖)和一个 server.js 入口。镜像体积可以从默认的 1G+ 缩到 200 MB 上下。

build 完成后磁盘上会出现:

.next/
├── standalone/ # 自包含运行产物(server.js + node_modules)
├── static/ # 静态资源(JS/CSS chunk),需要单独 COPY
└── ...
public/ # 你自己的静态资源,需要单独 COPY

.next/staticpublic/ 是 standalone 不会自动复制进 .next/standalone/ 的,这两个目录必须在 Dockerfile 里手动 COPY——这是最常见的疏漏,没复制的话部署上去 CSS / 图片全 404。

第二步:写多阶段 Dockerfile

项目根目录新建 Dockerfile

# ===== Stage 1: deps =====
FROM node:20-alpine AS deps
WORKDIR /app

# 仅复制 lock 文件,利用 Docker 缓存层
COPY package.json package-lock.json* ./
RUN npm ci

# ===== Stage 2: builder =====
FROM node:20-alpine AS builder
WORKDIR /app

COPY --from=deps /app/node_modules ./node_modules
COPY . .

# 关掉 Next.js 遥测(可选,加快构建)
ENV NEXT_TELEMETRY_DISABLED=1

RUN npm run build

# ===== Stage 3: runner =====
FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
ENV HOSTNAME=0.0.0.0

# 创建非 root 用户运行
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 nextjs

# standalone 产物 + 静态资源
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

CMD ["node", "server.js"]

几个关键细节:

  • HOSTNAME=0.0.0.0 必须加,Next.js standalone 默认监听 localhost,容器外访问不到
  • PORT=3000 和 CloudBase 云托管创建服务时填的端口要一致
  • node server.js 不是 npm start——standalone 入口就是 server.js,跳过 npm 一层启动更快
  • --chown=nextjs:nodejs 是为了让非 root 用户能读取这些文件;漏了会报 EACCES

本地验证一下镜像能跑通:

docker build -t my-nextjs:local .
docker run -p 3000:3000 -e NODE_ENV=production my-nextjs:local
# 浏览器打开 http://localhost:3000 确认页面正常

第三步:部署到云托管

用 CloudBase CLI 一行部署:

tcb login
tcb cloudrun deploy --port 3000

CLI 会问你三件事:

  1. 选择环境 ID
  2. 服务名(建议跟项目同名,例如 my-nextjs-app
  3. 是否启用公网访问(一般选是,否则只能内网访问)

CLI 会把当前目录打包上传,触发云端构建(云端用你的 Dockerfile build 镜像并部署)。整个过程一般几分钟。

部署完成后控制台「云托管 → 服务 → 你的服务名」能看到默认域名,形如 https://my-nextjs-app-abc123.ap-shanghai.app.tcloudbase.com

如果不想用 CLI,控制台还有两种入口:

  • 「通过本地代码部署」:上传 zip / 文件夹,云端 build
  • 「通过 Git 仓库部署」:绑定 GitHub / 工蜂,push 自动 build(推荐生产环境用这种,CI 能审计)

第四步:环境变量

Next.js 的环境变量分两种,这两种在云托管里要分别处理

类型命名何时生效怎么配
服务端运行时变量任意名容器启动时注入云托管「服务设置 → 环境变量」
客户端可见变量NEXT_PUBLIC_*next build 期间被静态嵌入到客户端 bundle必须在 build 时就有值(写进 Dockerfile 的 ENV,或 build 阶段从云端注入)

NEXT_PUBLIC_* 是 Next.js 的特殊约定:build 时会被替换成字符串硬编码到 JS 产物里。这意味着部署后改云托管控制台的 NEXT_PUBLIC_API_URL 不会生效——客户端 bundle 已经凝固了。要么重新 build,要么把变量做成运行时 fetch 拿配置。

绝对不要把 secret 放进 NEXT_PUBLIC_*,它会被打包进 JS 文件,浏览器 F12 一看就漏了。详细的密钥分层见 secure-secrets-in-cloud-function

服务端代码(Server Component / Route Handler / Server Action)里 process.env.SOME_SECRET 是安全的,那些代码不会进客户端 bundle。

云托管控制台配置环境变量:

  1. 进入服务详情 → 「服务设置」 → 「版本管理」 → 「新建版本」
  2. 在「环境变量」区域加 key/value
  3. 发布新版本,流量切到新版本

第五步:自定义域名 + HTTPS

云托管自带的 *.app.tcloudbase.com 域名能用,但生产环境一般要换成自己的。

  1. 控制台「云托管 → 服务 → 你的服务 → 自定义域名」点「添加域名」
  2. 填你的域名(例如 app.example.com),平台返回一段 CNAME 值
  3. 去你的 DNS 服务商加 CNAME 记录指向那段值
  4. 选证书:可选「自动申请免费证书」(基于 ACME,平台代签)或上传自己的证书
  5. 等域名状态变「已生效」(DNS 全球生效一般几分钟到一两小时)

域名生效后用 https://app.example.com 访问,Next.js 那边会自动收到 Host: app.example.com 的请求头。如果你在代码里检查 host(比如做多租户路由),记得在自定义域名生效前后都跑一遍。

运行验证

部署完后跑这几个:

# 1. 健康检查
curl -I https://your-service.app.tcloudbase.com/
# 预期: HTTP/2 200

# 2. SSR 页面(确认服务端渲染正常)
curl https://your-service.app.tcloudbase.com/ | grep -o '<title>[^<]*</title>'

# 3. 静态资源(确认 public/ 被正确 COPY)
curl -I https://your-service.app.tcloudbase.com/favicon.ico
# 预期: HTTP/2 200

# 4. _next/static(确认 .next/static 被正确 COPY)
# 在浏览器开发者工具 Network 里看 /_next/static/chunks/*.js 都是 200,不是 404

如果 /_next/static/... 是 404,回去检查 Dockerfile 里 COPY --from=builder /app/.next/static ./.next/static 这行——99% 是漏了。

常见错误

错误信息原因修复
部署成功但访问 503 / 容器启动失败镜像里 Next.js 监听了 localhost,端口暴露不出来Dockerfile 加 ENV HOSTNAME=0.0.0.0
CSS / 字体 / _next/static/*.js 全 404.next/static 没 COPY 进 runner stageDockerfile 加 COPY --from=builder /app/.next/static ./.next/static
/public/*.png 等图片 404public/ 没 COPYCOPY --from=builder /app/public ./public
改了 NEXT_PUBLIC_API_URL 重启服务后前端拿到的还是旧值NEXT_PUBLIC_* 是 build 时注入的,改控制台变量不会重新 build重新部署一次,或者改用运行时 fetch /api/config
Server Actions 调用失败,浏览器报 Server Action not found同一个 BUILD_ID 在多个实例间不一致(部分实例是旧版本)等所有实例切到新版本;或者云托管按"灰度"逐步切流量
Streaming 响应在浏览器里还是一次性出现中间某层(CDN / 自定义域名网关)开了响应缓冲自定义域名配置里关掉缓冲;本地 localhost 验证流式正常但线上不正常时几乎都是这个
镜像体积巨大(1G+)没开 output: 'standalone'node_modules 全打进去了next.config.jsoutput: 'standalone' 后重新 build
npm ciCannot find module ...package-lock.json 没提交,或本地 lock 和 package.json 不一致本地 npm install 后把 lock 一起提交;构建只用 npm ci 不要用 npm install

构建阶段的报错可以在云托管「服务详情 → 部署历史 → 查看日志」里看到完整堆栈。

相关文档

下一步

部署跑通后建议接着做:

  • add-vercel-ai-sdk-streaming-chatbot — 在 Next.js 上接一个流式 AI 聊天界面
  • connect-openai-api-cloud-function — 把 LLM API 代理放到云函数,前端通过云托管的 Next.js 调用
  • secure-secrets-in-cloud-function — 跨环境(dev / staging / prod)的密钥分层方案