把 Next.js 项目放到 VPS 上,最常见的结果是“首页能打开,刷新后图片 404”,或者开发环境正常、上线后环境变量失效。下面这套流程面向已有 Next.js 项目、package-lock.json 和 App Router:用 standalone 产物制作镜像,Docker Compose 管理进程,Caddy 接管 80/443 与 HTTPS。示例是单机部署,有短暂重建窗口;需要零停机时应另做双实例和共享缓存设计。
| 需求 | 更合适的方式 | 原因 |
|---|---|---|
| 纯静态页面、没有服务端功能 | 静态导出 + Web 服务器 | 少维护一个 Node.js 进程 |
| SSR、Route Handlers、Server Actions、ISR | 本文的 Node.js standalone 容器 | 这些功能需要 Next.js 服务端 |
| 多台 VPS、滚动发布 | 负载均衡 + 共享缓存与版本协调 | 单机 Compose 不能解决跨实例缓存一致性 |
如果你只是上线普通 Express 服务,可以看Node.js 项目 VPS 上线教程。这里重点处理 Next.js 的构建产物和运行特性。
本文示例用 Ubuntu 24.04、已安装 Docker Engine/Compose 插件的 VPS,以及解析到服务器公网 IP 的 app.example.com。建议从 2 vCPU / 4 GB 内存 / 40 GB SSD 起步;这是构建和运行的保守起点,不是 Next.js 官方最低要求。大型项目在 next build 时可能需要 8 GB 或在 CI 中构建镜像。还要留足镜像、日志和回滚版本的磁盘空间。云安全组与主机防火墙开放 80/443,SSH 只允许管理来源;不要把 3000 端口开放到公网。
先确认应用本身可以本地执行 npm ci、npm run build。本文统一使用 npm lockfile;使用 pnpm/Yarn 的项目需要按自己的锁文件改 Dockerfile,不能把安装命令混用。
在项目根目录的 next.config.ts 中加入 standalone 输出;如果已有其他配置,只合并这一项:
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
output: 'standalone',
}
export default nextConfig
给容器一个不访问数据库的健康检查端点 app/api/health/route.ts:
export async function GET() {
return Response.json({ status: 'ok' })
}
这个端点只说明 Web 进程能回答请求。数据库、队列或外部 API 的可用性应另设监控,不要把每个外部依赖都塞进容器存活检查,否则短暂故障可能触发无意义的重启。
项目根目录新建 Dockerfile:
FROM node:24-bookworm-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --no-audit --no-fund
FROM node:24-bookworm-slim AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN mkdir -p public && npm run build
FROM node:24-bookworm-slim AS runner
WORKDIR /app
ENV NODE_ENV=production PORT=3000 HOSTNAME=0.0.0.0
RUN mkdir -p .next && chown node:node .next
COPY --from=builder --chown=node:node /app/public ./public
COPY --from=builder --chown=node:node /app/.next/standalone ./
COPY --from=builder --chown=node:node /app/.next/static ./.next/static
USER node
EXPOSE 3000
CMD ["node", "server.js"]
官方的 standalone 产物不会自动包含 public 和 .next/static。漏复制时,首页可能正常,但 CSS、JS 或图片路径会 404。这里运行容器使用非 root 用户;如果应用需要写上传文件,改用专门的数据卷与外部对象存储,不要直接往镜像目录写。
新建 .dockerignore,避免把本地依赖、密钥和构建结果送进构建上下文:
node_modules
.next
.git
.env*
app.env
*.log
在项目根目录创建 compose.yaml:
services:
app:
image: nextjs-app:${APP_VERSION}
build: .
init: true
restart: unless-stopped
env_file:
- ./app.env
environment:
NODE_ENV: production
HOSTNAME: 0.0.0.0
PORT: '3000'
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://127.0.0.1:3000/api/health', r => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
mem_limit: 1g
cpus: '1.5'
pids_limit: 256
security_opt:
- no-new-privileges:true
caddy:
image: caddy:2
restart: unless-stopped
depends_on:
app:
condition: service_healthy
environment:
DOMAIN: ${DOMAIN}
ports:
- '80:80'
- '443:443'
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
volumes:
caddy_data:
caddy_config:
再建 Caddyfile:
{$DOMAIN} {
encode zstd gzip
reverse_proxy app:3000
}
Compose 的 .env 用来给配置文件插值,app.env 专门给 Next.js 服务端运行时读取,二者用途不同:
# .env
DOMAIN=app.example.com
APP_VERSION=2026-09-25-a1b2c3d
# app.env:仅示例;替换为你的真实服务端变量
API_BASE_URL=https://api.example.com
把 .env、app.env 加入 .gitignore,在 VPS 上设为只有部署用户可读:chmod 600 .env app.env。不要把数据库密码写成 NEXT_PUBLIC_ 变量:这类变量会进入浏览器包,而且值在 next build 时固定。更改 NEXT_PUBLIC_ 后必须重新构建镜像;服务端变量是否能在运行时读取,仍取决于代码有没有在构建阶段使用它。
DNS 生效后,在项目目录执行:
docker compose config --quiet
docker compose build app
docker compose up -d
docker compose ps
docker compose logs --tail=100 app caddy
curl -I https://app.example.com/
docker compose config --quiet 只检查配置语法,不能证明镜像构建或域名证书成功。Caddy 需要域名指向这台 VPS,且公网能访问 80/443 才能自动申请证书。若主机已有 Nginx、宝塔或其他服务占用这两个端口,先决定谁负责入口,别直接在同一台机器上抢端口。已有 Caddy 入口时,只给原有 Caddy 增加站点反代即可。
上线时把 APP_VERSION 设为提交号或发布号,不要每次都复用 latest。更新代码和版本号后运行:
docker compose build app
docker compose up -d --no-deps app
docker compose ps
curl -fsS https://app.example.com/api/health
Compose 会重建单个应用容器,可能有短暂中断。若新版本健康检查失败,把 .env 的 APP_VERSION 改回上一版本,再运行 docker compose up -d --no-deps app;前提是旧镜像还在本机,回滚前不要执行镜像清理。如果新版本改动了数据库结构,回滚应用并不自动回滚数据迁移,必须先设计兼容迁移或恢复方案。
备份至少包括 Git 代码仓库、部署用 .env/app.env 的安全副本,以及应用真正的持久化数据(数据库、用户上传、对象存储)。Next.js 镜像可以重建,.next 缓存一般不应当当作唯一数据源。备份要放到另一台机器或对象存储,并定期在隔离环境做一次恢复演练。想进一步设置 Docker 资源与日志策略,可参照Docker Compose 生产环境配置清单。
| 现象 | 先查什么 | 常见处理 |
|---|---|---|
| 域名返回 502 | docker compose ps、docker compose logs app | 应用构建失败、容器不健康或 Caddy 上游地址写错 |
/_next/static/* 404 | 镜像内是否有 /app/.next/static | 检查 Dockerfile 的静态资源 COPY,然后重建 |
public 图片 404 | 镜像内是否有 /app/public | 检查文件大小写及 COPY;Linux 路径区分大小写 |
| 新环境变量不生效 | 变量是否以 NEXT_PUBLIC_ 开头 | 客户端变量重新 build;服务端变量检查构建阶段使用位置 |
| 构建进程被杀 | free -h、dmesg、docker system df | 扩内存或在 CI 构建;清理前确认旧镜像与数据卷 |
| HTTPS 证书拿不到 | DNS A/AAAA、80/443、防火墙与 Caddy 日志 | 修正解析,释放端口,再观察自动签发日志 |
官方参考:Next.js 自托管说明、Next.js Docker standalone 示例、Docker Compose 生产部署、Caddy 自动 HTTPS。准备上线前,用真实域名跑通一次构建、健康检查、静态资源与回滚;这四项比只看首页能否打开更有用。
