把 Next.js 14+ App Router 应用部署到 CloudBase 云托管
一句话定义:用
next.config.js的output: '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.js | 14.x 或 15.x |
| Docker(本地构建验证) | latest |
@cloudbase/cli | latest |
| 一个已开通的 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/static 和 public/ 是 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 /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 /app/public ./public
COPY /app/.next/standalone ./
COPY /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 会问你三件事:
- 选择环境 ID
- 服务名(建议跟项目同名,例如
my-nextjs-app) - 是否启用公网访问(一般选是,否则只能内网访问)
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。
云托管控制台配置环境变量:
- 进入服务详情 → 「服务设置」 → 「版本管理」 → 「新建版本」
- 在「环境变量」区域加 key/value
- 发布新版本,流量切到新版本
第五步:自定义域名 + HTTPS
云托管自带的 *.app.tcloudbase.com 域名能用,但生产环境一般要换成自己的。
- 控制台「云托管 → 服务 → 你的服务 → 自定义域名」点「添加域名」
- 填你的域名(例如
app.example.com),平台返回一段 CNAME 值 - 去你的 DNS 服务商加 CNAME 记录指向那段值
- 选证书:可选「自动申请免费证书」(基于 ACME,平台代签)或上传自己的证书
- 等域名状态变「已生效」(DNS 全球生效一般几分钟到一两小时)
域名生效后用 https://app.example.com 访问,Next.js 那边会自动收到 Host: app.example.com 的请求头。如果你在代码里检查 host(比如做多租户路由),记得在自定义域名生效前后都跑一遍。