一、基础设施
| 组件 | 值 |
|---|
| 服务器 | AWS EC2 52.77.230.213 |
| 用户 | ec2-user |
| SSH 密钥 | c:\Users\chang.ssh\sg-xiangyu.pem |
| 域名 | wescores.net(HTTPS) |
| 项目源码 | C:\projects\stativo |
| 项目路径(服务器) | /var/www/stativo/ |
| 部署脚本 | scripts/deploy-remote.sh |
| PM2 配置 | ecosystem.config.js |
| 环境变量 | .env.production(不在 Git 中) |
二、运行架构
1
2
3
4
5
| 用户 → HTTPS(443) → Nginx → proxy_pass http://127.0.0.1:3000
↓
PM2 集群模式(4实例)
↓
Next.js Standalone Server
|
三、服务器目录结构
1
2
3
4
5
6
7
8
9
10
11
| /var/www/stativo/
├── current/ → 符号链接,指向最新 release
├── previous/ → 符号链接,指向上一个 release(回滚用)
├── releases/ → 按时间戳命名的发布版本(保留最近3个)
│ ├── 20260918112041/ → 最新部署
│ │ ├── .next/standalone/ ← PM2 实际运行入口
│ │ ├── .env.production ← 环境配置
│ │ ├── ecosystem.config.js ← PM2 配置
│ │ └── scripts/deploy-remote.sh ← 部署脚本
│ └── ...
└── shared/ ← SSL 验证等共享文件
|
四、关键配置文件
4.1 deploy-remote.sh(部署脚本)
位置:scripts/deploy-remote.sh
8 步流程:
| 步骤 | 操作 | 说明 |
|---|
| 1/8 | 解压源码 | tar xzf 到新 release 目录,删除原始包 |
| 2/8 | 复制环境变量 | 优先 /tmp/.env.production > shared > current |
| 3/8 | pnpm install | –frozen-lockfile,锁定依赖版本 |
| 4/8 | pnpm build | source .env.production 后构建 |
| 5/8 | 验证 standalone | 检查 server.js 存在,复制 static + public |
| 6/8 | 切换 current | 旧 current 存为 previous,新 release 链为 current |
| 7/8 | PM2 重启 | 删除旧进程,从新 release 的 ecosystem.config.js 启动 |
| 8/8 | 健康检查 | curl 127.0.0.1:3000,2xx 即成功,清理旧 release |
失败时输出回滚命令:
1
| ln -sfn $(readlink /var/www/stativo/previous) /var/www/stativo/current && pm2 reload stativo
|
4.2 ecosystem.config.js(PM2 配置)
位置:ecosystem.config.js
- 4 实例集群模式(cluster)
- 端口 3000
- HOSTNAME 强制 0.0.0.0(避免系统 HOSTNAME 干扰健康检查)
- 内存重启阈值 1G
- 日志模式可配:off / error / all(默认 off,写 /dev/null)
- 环境变量由部署脚本注入:PM2_APP_NAME、PM2_APP_PORT、PM2_APP_CWD
4.3 next.config.ts(构建配置)
typescript.ignoreBuildErrors: true(临时关闭,遗留类型错误较多,后续修复后改回 false)eslint.ignoreDuringBuilds: trueoutputFileTracingIncludes 包含 jose 模块(Next.js standalone 不自动追踪)- standalone 模式构建
4.4 Nginx 配置
位置:/etc/nginx/conf.d/stativo.conf
- proxy_pass 指向 127.0.0.1:3000
- SSL 证书:/etc/pki/tls/certs/wescores.net-origin.pem
- HTTPS 443 → HTTP 3000 反向代理
4.5 .env.production(环境变量)
关键配置:
1
2
3
4
5
6
7
| NEXT_PUBLIC_SITE_NAME=wescores
NEXT_PUBLIC_SITE_DOMAIN=wescores.net
NEXT_PUBLIC_SITE_URL=https://wescores.net
NEXT_PUBLIC_API_URL=https://api.6wn.net/api
AUTH_API_URL=https://api.6wn.net/api
AUTH_TRUST_HOST=true
NEXTAUTH_URL=https://wescores.net
|
五、更新部署流程
5.1 前置条件
必须先提交代码。 部署基于 git archive HEAD,只有已提交的代码才会参与部署。
1
2
3
4
| cd C:\projects\stativo
git add .
git commit -m "type(scope): description"
git status # 确认 "working tree clean"
|
5.2 方式一:说"更新部署发布"
先装个 AI 搭档
我推荐 [Trae] (https://www.trae.cn/?utm_source=advertising&utm_medium=xiaolincoding_ug_cpa&utm_term=hw_trae_xiaolincoding, “中文友好,界面简洁”)。
对trae-ai说"更新部署发布",trae自动执行全部步骤:
git status 确认工作区干净git archive --format=tar.gz HEAD 打包最新提交scp 上传三个文件到服务器 /tmp:- stativo-deploy.tar.gz(代码包)
- .env.production(环境变量)
- scripts/deploy-remote.sh(部署脚本)
- SSH 执行
bash /tmp/deploy-remote.sh - 等待 8 步完成,检查 DEPLOY_SUCCESS 或 HEALTH_FAIL
- 验证 https://wescores.net 返回 200
- 清理本地临时文件
5.3 方式二:手动部署
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| # 0. 确保代码已提交
cd C:\projects\stativo
git status # 应显示 "working tree clean"
# 1. 打包代码(git archive HEAD,只含已提交代码)
git archive --format=tar.gz HEAD -o "$env:TEMP\stativo-deploy.tar.gz"
# 2. 上传三个文件
scp -i "c:\Users\chang\.ssh\sg-xiangyu.pem" -o ConnectTimeout=15 -o ServerAliveInterval=5 `
"$env:TEMP\stativo-deploy.tar.gz" ec2-user@52.77.230.213:/tmp/
scp -i "c:\Users\chang\.ssh\sg-xiangyu.pem" -o ConnectTimeout=15 -o ServerAliveInterval=5 `
"C:\projects\stativo\.env.production" ec2-user@52.77.230.213:/tmp/
scp -i "c:\Users\chang\.ssh\sg-xiangyu.pem" -o ConnectTimeout=15 -o ServerAliveInterval=5 `
"C:\projects\stativo\scripts\deploy-remote.sh" ec2-user@52.77.230.213:/tmp/
# 3. 执行部署
ssh -i "c:\Users\chang\.ssh\sg-xiangyu.pem" -o ConnectTimeout=15 -o ServerAliveInterval=5 `
ec2-user@52.77.230.213 "bash /tmp/deploy-remote.sh"
|
5.4 部署成功标志
1
2
3
4
5
6
7
8
9
| [1/8] extracting... ✓
[2/8] copying env... ✓
[3/8] pnpm install... ✓
[4/8] pnpm build... ✓
[5/8] verifying standalone... ✓
[6/8] switching current... ✓
[7/8] PM2 restart... ✓ (4 instances online)
[8/8] health check... HEALTH_OK: HTTP 200
DEPLOY_SUCCESS: /var/www/stativo/releases/YYYYMMDDHHMMSS
|
5.5 部署失败处理
如果 HEALTH_FAIL,脚本会输出回滚命令,手动执行:
1
2
| ssh -i "c:\Users\chang\.ssh\sg-xiangyu.pem" ec2-user@52.77.230.213 `
"ln -sfn `$(readlink /var/www/stativo/previous) /var/www/stativo/current && pm2 reload stativo"
|
常见失败原因:
- TypeScript 类型错误 → next.config.ts 当前已临时关闭
- pnpm install 失败 → 检查网络或 lockfile
- standalone server.js 缺失 → 构建失败
- 健康检查超时 → 检查 PM2 日志或端口冲突
六、同步环境变量(单独更新 .env.production)
修改了 .env.production 但不想重新部署代码时:
1
2
3
4
5
6
| # 上传到服务器并同步到 shared 目录
scp -i "c:\Users\chang\.ssh\sg-xiangyu.pem" `
"C:\projects\stativo\.env.production" ec2-user@52.77.230.213:/tmp/.env.production
ssh -i "c:\Users\chang\.ssh\sg-xiangyu.pem" ec2-user@52.77.230.213 `
"sudo cp /tmp/.env.production /var/www/stativo/shared/.env.production && sudo chown ec2-user:ec2-user /var/www/stativo/shared/.env.production && rm /tmp/.env.production"
|
注意:环境变量更新后需要重新部署或重启才能生效:
1
| ssh -i "c:\Users\chang\.ssh\sg-xiangyu.pem" ec2-user@52.77.230.213 "pm2 reload stativo --update-env"
|
七、回滚到上一版本
1
2
| ssh -i "c:\Users\chang\.ssh\sg-xiangyu.pem" ec2-user@52.77.230.213 `
"ln -sfn `$(readlink /var/www/stativo/previous) /var/www/stativo/current && pm2 reload stativo"
|
八、常用运维命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| # SSH 连接服务器
ssh -i "c:\Users\chang\.ssh\sg-xiangyu.pem" ec2-user@52.77.230.213
# 查看 PM2 状态
pm2 list
# 查看实时日志
pm2 logs stativo --lines 50
# 重启应用(不重新部署)
pm2 reload stativo --update-env
# 检查 Nginx 配置
sudo nginx -t && sudo systemctl reload nginx
# 查看健康状态
curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000
curl -s -o /dev/null -w '%{http_code}' https://wescores.net
# 查看当前 release
readlink -f /var/www/stativo/current
# 查看所有 releases
ls -lt /var/www/stativo/releases/
# 查看磁盘空间
df -h
|
九、注意事项
必须先提交代码:git archive HEAD 只打包已提交代码,未提交的修改不会部署。部署前确保 git status 显示 “working tree clean”。
.env.production 不在 Git 中:.gitignore 的 .env* 规则排除了它。部署脚本从 /tmp/.env.production 读取,每次部署都会上传本地版本。
deploy-remote.sh 随代码一起部署:脚本在 scripts/ 目录下,git archive 会包含它。但实际执行的是上传到 /tmp/deploy-remote.sh 的版本。
TypeScript 类型检查已临时关闭:next.config.ts 中 ignoreBuildErrors: true,遗留类型错误不阻断构建。修复完所有类型错误后改回 false。
端口统一为 3000:PM2、Nginx proxy_pass、Docker 配置全部使用 3000。
HOSTNAME 强制 0.0.0.0:ecosystem.config.js 中强制设置,避免系统 HOSTNAME(如 ip-172-31-xx-xx)导致应用只绑定内网 IP,健康检查和 Nginx 代理失效。
十、已踩过的坑
| 问题 | 原因 | 解决方案 |
|---|
| jose 模块缺失 | Next.js standalone 未自动追踪 | next.config.ts 添加 outputFileTracingIncludes |
| localhost:3001 泄露 | 源码 defaultConfig 默认值被编译进产物 | 运行时被 production 配置覆盖,不影响实际运行 |
| .env.production 不同步 | .gitignore 排除,服务器旧版覆盖新版 | 部署脚本改为优先从 /tmp/.env.production 读取 |
| AUTH_TRUST_HOST 缺失 | NextAuth v5 生产环境必需 | 已添加到 .env.production |
| HTTPS 502 | Nginx proxy_pass 端口与 PM2 不一致 | 统一为 3000 |
| PM2 启动失败 | 日志目录权限 | sudo chown -R ec2-user:ec2-user /var/log/stativo |
| HOSTNAME 绑定内网 IP | 系统 HOSTNAME 环境变量覆盖 | ecosystem.config.js 强制 HOSTNAME=0.0.0.0 |
| TypeScript 类型错误 | 代码有类型不兼容问题 | 临时关闭 ignoreBuildErrors,后续逐个修复 |
| 未提交代码未部署 | git archive HEAD 只含已提交代码 | 部署前必须 git commit |
十一、Docker 配置(备用)
项目在 .docker/ 目录下保留了完整的 Docker 部署配置,端口已统一为 3000,当前未启用。
11.1 文件清单
| 文件 | 说明 |
|---|
.docker/Dockerfile | 多阶段构建(deps → builder → runner),Alpine + dumb-init |
.docker/docker-compose.yml | 容器编排,含应用容器 + Nginx + Certbot(SSL profile) |
.docker/env.docker | Docker 环境变量(从 .env.production 复制,不提交 Git) |
.docker/env.example | 环境变量模板(已更新为 wescores.net 域名) |
.docker/deploy.sh | Docker 一键部署脚本(git pull + docker compose) |
.docker/init-ssl.sh | Let’s Encrypt SSL 证书首次签发脚本(6 步自动化) |
.docker/nginx/nginx.conf | Docker Nginx 配置(HTTP 重定向 + HTTPS 反向代理 + 静态缓存) |
.docker/deploy-guide.html | Docker 部署可视化指南 |
.dockerignore | 排除依赖、构建产物、secrets |
11.2 Dockerfile 多阶段构建说明
1
2
3
| Stage 1: deps → 安装全量依赖(含 devDependencies),BuildKit 缓存 pnpm store
Stage 2: builder → 复制源码,pnpm run build:prod,BuildKit 缓存 .next/cache
Stage 3: runner → Alpine + dumb-init,复制 standalone 产物,非 root 用户运行
|
关键设计:
- BuildKit 缓存挂载:pnpm store 和 .next/cache 跨构建复用,增量构建快
- 依赖层与源码层分离:改代码不重装依赖
- standalone 模式:镜像不含 node_modules,仅 standalone 追踪的文件
- outputFileTracingIncludes 已在 next.config.ts 配置 jose 模块
- dumb-init 作为 PID 1,正确转发 SIGTERM 实现优雅退出
11.3 两种 Docker 部署模式
模式 A:纯容器(复用系统 Nginx + SSL)
Docker 只跑应用容器,SSL 和域名继续用系统 Nginx(与 PM2 方案一致)。
适用场景:服务器已有系统 Nginx + SSL 证书,只想把进程管理从 PM2 换成 Docker。
模式 B:全容器(Docker Nginx + Certbot 自动 SSL)
Docker 管理全部:应用容器 + Nginx 反向代理 + Let’s Encrypt 证书自动签发和续期。
适用场景:全新服务器,不想手动配 Nginx 和 SSL,一切交给 Docker Compose。
11.4 切换到 Docker 部署的完整步骤
前置准备
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| # 1. SSH 连接服务器
ssh -i "c:\Users\chang\.ssh\sg-xiangyu.pem" ec2-user@52.77.230.213
# 2. 安装 Docker + Docker Compose v2(如未安装)
sudo yum update -y
sudo yum install -y docker
sudo systemctl enable docker
sudo systemctl start docker
sudo usermod -aG docker ec2-user
# 重新登录使 docker 组生效
exit
ssh -i "c:\Users\chang\.ssh\sg-xiangyu.pem" ec2-user@52.77.230.213
docker --version # 验证安装
# 3. git clone 项目(如服务器上没有)
cd ~
git clone <仓库地址> stativo
cd ~/stativo
|
创建环境变量
1
2
3
4
5
6
7
8
9
10
| cd ~/stativo/.docker
cp env.example env.docker
vi env.docker
# 填入真实值,参考 .env.production(域名 wescores.net、API 地址、鉴权密钥等)
# 关键项:
# NEXT_PUBLIC_SITE_URL=https://wescores.net
# NEXTAUTH_URL=https://wescores.net
# AUTH_TRUST_HOST=true
# NEXTAUTH_SECRET=NIUGOyvfaUH2Aq5DA75E9+Lh/nUy7jxxrvK2qeHdLwg=
# AUTH_GOOGLE_ID=428668096460-mdonea4qtvcoacvqru8vrvf8mkia9n75.apps.googleusercontent.com
|
停止 PM2(如从 PM2 方案切换)
1
2
3
4
| pm2 delete stativo
pm2 save
# 可选:如不再使用 PM2,卸载
# npm uninstall -g pm2
|
模式 A:纯容器部署
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| cd ~/stativo
# 拉取最新代码
git pull origin main
# 构建并启动(不启用 SSL profile)
docker compose -f .docker/docker-compose.yml up -d --build
# 等待健康检查(约 40 秒)
docker compose -f .docker/docker-compose.yml ps
# 状态应显示 "healthy"
# 验证
curl -s -o /dev/null -w '%{http_code}' http://localhost:3000
# 应返回 200
|
系统 Nginx proxy_pass 指向 127.0.0.1:3000,与 PM2 方案一致,无需修改 Nginx 配置。
模式 B:全容器部署(含 SSL)
首次签发 SSL 证书:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| cd ~/stativo
# 确保域名 DNS 已指向服务器
dig wescores.net # 应解析到 52.77.230.213
# 停止占用 80 端口的服务
sudo systemctl stop nginx # 如有系统 Nginx
# 签发证书(6 步自动化)
bash .docker/init-ssl.sh
# 脚本会:
# 1. 停止现有容器
# 2. 创建 certbot 目录
# 3. 启动临时 Nginx 处理 ACME 验证
# 4. Certbot 签发证书
# 5. 清理临时 Nginx
# 6. 验证证书文件
|
启动完整部署:
1
2
3
4
5
6
7
8
9
10
11
12
| cd ~/stativo
# 启动(含 SSL profile:应用 + Nginx + Certbot)
docker compose -f .docker/docker-compose.yml --profile ssl up -d --build
# 查看容器状态
docker compose -f .docker/docker-compose.yml --profile ssl ps
# 应看到 3 个容器:stativo-web (healthy), stativo-nginx, stativo-certbot
# 验证
curl -s -o /dev/null -w '%{http_code}' https://wescores.net
# 应返回 200
|
SSL 证书自动续期:
Certbot 容器会每 12 小时自动检查并续期证书,无需手动操作。证书路径:
1
2
3
| .docker/certbot/conf/live/wescores.net/
├── fullchain.pem
└── privkey.pem
|
11.5 Docker 日常更新部署
1
2
3
4
5
6
| # 服务器上执行
cd ~/stativo
git pull origin main
docker compose -f .docker/docker-compose.yml up -d --build
# 或如使用 SSL profile:
# docker compose -f .docker/docker-compose.yml --profile ssl up -d --build
|
或从本地一键触发(使用 deploy.sh):
1
2
| cd C:\projects\stativo
ssh -i "c:\Users\chang\.ssh\sg-xiangyu.pem" ec2-user@52.77.230.213 "cd ~/stativo && git pull origin main && docker compose -f .docker/docker-compose.yml up -d --build"
|
11.6 Docker 常用运维命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| # 查看容器状态
docker compose -f .docker/docker-compose.yml ps
# 含 SSL:docker compose -f .docker/docker-compose.yml --profile ssl ps
# 查看应用日志
docker compose -f .docker/docker-compose.yml logs -f stativo
# 重启应用容器
docker compose -f .docker/docker-compose.yml restart stativo
# 停止全部容器
docker compose -f .docker/docker-compose.yml down
# 含 SSL:docker compose -f .docker/docker-compose.yml --profile ssl down
# 重建(无缓存,解决构建缓存问题)
docker compose -f .docker/docker-compose.yml build --no-cache
docker compose -f .docker/docker-compose.yml up -d
# 进入容器调试
docker exec -it stativo-web sh
# 查看容器资源使用
docker stats stativo-web
# 清理构建缓存(释放磁盘)
docker builder prune -f
# 清理悬空镜像
docker image prune -f
|
11.7 Docker 与 PM2 方案对比
| 维度 | PM2(当前) | Docker |
|---|
| 服务器依赖 | pnpm + node + pm2 + curl | Docker + Docker Compose |
| 环境一致性 | 服务器直接构建 | ✅ 容器隔离,多环境一致 |
| 多实例 | ✅ 4 实例集群 | ❌ 单实例(需配 replicas 或 nginx upstream) |
| 部署命令 | git archive + scp + ssh | git pull + docker compose up |
| 构建速度 | 服务器全量 build | BuildKit 缓存,增量快 |
| 资源限制 | PM2 max_memory_restart | ✅ 内存/CPU 硬限制 |
| 日志管理 | pm2 logs | docker logs(自动轮转) |
| 回滚 | previous 软链,秒级回滚 | 切镜像 tag |
| SSL 管理 | 手动配置系统 Nginx | ✅ Certbot 自动签发续期 |
| 部署方式 | 打包上传(不依赖服务器 git) | 服务器 git pull(需仓库访问权限) |
11.8 Docker 部署注意事项
端口冲突:模式 A 和 B 不能同时使用。模式 B 的 Nginx 容器会占用 80/443 端口,与系统 Nginx 冲突。
env.docker 不提交 Git:.dockerignore 已排除 env.docker 和 .docker/env.docker,需要在服务器上手动创建。
git pull 部署 vs git archive 部署:Docker 的 deploy.sh 使用 git pull,需要服务器有仓库访问权限(部署密钥或 HTTPS 认证)。PM2 方案用 git archive 打包上传,不需要服务器访问 Git 仓库。
数据卷:SSL 证书存在 .docker/certbot/conf/ 目录,Docker 容器重建不会丢失证书。但 docker compose down 删除容器后,目录文件仍在。
内存限制:docker-compose.yml 配置了 512M 内存上限。如应用 OOM,调整 deploy.resources.limits.memory。
从 PM2 迁移到 Docker:必须先 pm2 delete stativo 释放端口 3000,否则 Docker 容器端口映射会失败。