Payload CMS 3 + Next.js 16 部署踩坑记录
一、本地开发阶段
1.1 Payload CLI ESM 报错
现象:payload generate:types 在 Node 20/22 下崩溃,报 ERR_REQUIRE_ASYNC_MODULE。
解法:这是 Payload CLI 的已知问题,目前没有官方修复。手动维护类型文件。
1.2 pnpm strict 模式下模块找不到
现象:从项目代码里引用 @payloadcms/ui 时报模块不存在,即使它是 @payloadcms/next 的间接依赖。
解法:pnpm 严格模式要求间接依赖必须在 package.json 中显式声明。把 @payloadcms/ui 加为直接依赖即可。
1.3 Next.js 16 升级后各种报错
现象:升级到 Next.js 16 后,布局嵌套、@payloadcms/next/css 导入路径、Turbopack 配置等多处报错。
解法:升级前务必先看 node_modules/next/dist/docs/ 下的文档,逐条对照变更。
二、构建阶段
2.1 Docker 构建时连不上数据库
现象:pnpm build 失败,报 Error: cannot connect to SQLite。
解法:Payload v3 在 next build 期间初始化数据库适配器,而 Docker 构建环境没有数据库。将 build 命令改为 compile 模式,跳过预渲染:
"build": "next build --experimental-build-mode compile"
这样只编译代码不预渲染,不需要数据库连接。对于 CMS 站点反而是更好的选择——所有页面在请求时按需渲染,每次拿到最新数据。
2.2 NEXT_PUBLIC_ 变量在构建时被写死
现象:本地开发正常,部署到 ECS 后 admin 面板保存报 403: You are not allowed to perform this action,CSRF 配置源码里明明是对的。
解法:NEXT_PUBLIC_* 变量在构建时被内联到编译产物中,Docker 构建时 .env.local 不在上下文里,值为空并回退到默认值。服务端配置不要用 NEXT_PUBLIC_ 前缀:
// ❌ 构建时内联,值被写死const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'// ✅ 运行时从容器环境变量读取const SITE_URL = process.env.SITE_URL || 'http://localhost:3000'
2.3 Mac 构建的镜像跑在 ECS 上
现象:no image found in image index for architecture amd64。
解法:Mac 默认构建 arm64 镜像,ECS 是 amd64。构建脚本里指定平台:
docker build --platform linux/amd64 ...
三、部署阶段
3.1 SQLite 生产环境表不存在
现象:容器启动后日志报 SQLITE_ERROR: no such table: users。
解法:push: true 只在开发环境生效,生产环境被显式忽略。用 Payload 的 prodMigrations:
npx payload migrate:create init --force-accept-warning --skip-empty
import { sqliteAdapter } from '@payloadcms/db-sqlite'import { migrations } from './migrations'export default buildConfig({ db: sqliteAdapter({ client: { url: process.env.DATABASE_URI || 'file:./db/database.db' }, prodMigrations: migrations, }),})
容器启动时自动运行未执行的 migration,后续 schema 变更跑 npx payload migrate:create 重新部署即可。
3.2 SQLite 目录权限拒绝
现象:容器启动但崩溃,报 Error: cannot connect to SQLite: ConnectionFailed("Unable to open connection to local database ./db/database.db: 14")。
解法:容器以 uid 1000 的 node 用户运行,但宿主机目录属于 root。部署前改权限:
mkdir -p /opt/notes/db && chown 1000:1000 /opt/notes/db
3.3 容器名冲突
现象:docker compose up -d 报 the container name "notes-app" is already in use。
解法:--remove-orphans 只管当前 compose project 的容器,跨项目的不会清理。先手动删除:
docker stop notes-app notes-nginx 2>/dev/null || truedocker rm notes-app notes-nginx 2>/dev/null || truedocker compose up -d --remove-orphans
3.4 docker compose ps 报变量未定义
现象:docker compose ps 报 service "app" has neither an image nor a build context specified。
解法:compose 即使 ps 也会解析整个 compose.yaml,image: ${APP_IMAGE} 变量未 export 就报错。用 docker inspect 替代:
PREV_IMAGE=$(docker inspect --format='{{.Config.Image}}' notes-app 2>/dev/null || echo "none")
3.5 Nginx 启动即退出
现象:notes-nginx 容器 exit code 1,端口不可达。
解法:certs/ 在 .gitignore 里,ECS 上没有证书文件。SSH 到服务器生成自签证书:
cd /opt/notes/docker/productionmkdir -p certsopenssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ -keyout certs/key.pem -out certs/cert.pem \ -subj "/CN=your-server-ip"docker restart notes-nginx
等有正式域名再换 Let's Encrypt 证书。别用本地脚本生成的 CN=localhost 证书——ECS 上没用。
四、上线后阶段
4.1 HTTPS 端口不通
现象:HTTP(80)正常,HTTPS(443)超时。服务器防火墙没问题,Nginx 内部正常。
解法:阿里云安全组只开了 80,忘了加 443。在安全组入方向规则里加一条 TCP 443 放行。
4.2 Cookie 认证在 Chrome 下静默失败
现象:登录返回 HTTP 200 + 合法 JWT,但浏览器立刻重定向回登录页。环境变量、数据库、Nginx 配置都没问题。
解法:Payload cookie 认证依赖 Origin 或 Sec-Fetch-Site 头做 CSRF 校验,Chrome AJAX 在 HTTP 下不发这两个头,导致 cookie 认证被静默拒绝。在 Nginx 层注入 Origin 头:
proxy_set_header Origin "http://$host";
不改应用代码,版本升级也不怕。上了 HTTPS 后浏览器自然会带 Sec-Fetch-Site: same-origin,这行就可以去掉了。
4.3 部署失败后回滚不执行
现象:docker compose up -d 失败后服务挂了,回滚代码没跑。
解法:set -e 下命令失败 shell 立即退出,后续回滚逻辑永远不会执行。把回滚逻辑放在健康检查之后作为独立条件判断,对可能失败的命令用 || true 允许失败。
4.4 Podman 和 Docker 凭据不互通
现象:podman login 成功,但 docker-compose pull 报 denied。
解法:Podman 和 Docker 有各自独立的凭据存储。部署脚本里同时登录两个,或生成统一的 config.json。
4.5 国内 Docker Hub 拉镜像慢
现象:docker pull 极慢或超时。
解法:配置阿里云 ACR 镜像加速,修改 /etc/docker/daemon.json:
{ "registry-mirrors": ["https://<your-region>.mirror.aliyuncs.com"]}
改完重启 Docker:
sudo systemctl daemon-reload && sudo systemctl restart docker