直接部署
服务器环境准备
安装 Node.js
# 使用 nvm 安装
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
nvm install 20
nvm use 20为什么用 nvm?
nvm 允许多版本 Node.js 共存,方便升级和回退。直接 apt install nodejs 通常安装的是旧版本。
Node.js 版本选择
推荐 LTS 版本(偶数版本,如 20、22)。Nuxt 4 需要 Node.js ≥ 18。生产环境不要用奇数版本(如 21、23),它们不是 LTS。
安装 PostgreSQL
# Ubuntu
sudo apt update
sudo apt install postgresql postgresql-contrib
# 创建数据库和用户
sudo -u postgres psql
CREATE USER myuser WITH PASSWORD 'mypassword';
CREATE DATABASE flutter_api OWNER myuser;数据库密码强度
mypassword 只是示例!生产环境使用 openssl rand -hex 16 生成强密码。
安装 Redis
sudo apt install redis-server
sudo systemctl enable redis-serverRedis 安全加固:
# /etc/redis/redis.conf
requirepass your-strong-password # 设置密码
bind 127.0.0.1 # 只监听本地为什么 Redis 要设密码?
默认 Redis 无密码且监听所有接口。如果服务器有公网 IP,攻击者可以直接连接 Redis 执行任意命令(如写入 SSH 密钥)。
部署步骤
1. 上传代码
# 方式一:Git
git clone https://github.com/your-org/flutter-api-server.git
cd flutter-api-server
# 方式二:rsync(从本地推送)
rsync -avz --exclude node_modules ./ user@server:/app/flutter-api-server/为什么排除 node_modules?
本地的 node_modules 可能包含开发依赖和平台特定的二进制文件。服务器上 npm ci 会安装正确的生产依赖。
2. 安装依赖
npm cinpm ci vs npm install?
npm ci 严格按照 package-lock.json 安装,确保版本一致。npm install 可能自动升级补丁版本,导致生产环境与开发环境不一致。
3. 配置环境变量
cp .env.example .env
nano .env # 编辑环境变量4. 数据库迁移
npx drizzle-kit migrate迁移前备份数据库!
迁移可能修改表结构,如果出错需要回滚。参考"上线与运维"章节的备份策略。
5. 构建
npm run build构建输出在 .output/ 目录
nuxt build 生成 .output/server/index.mjs(服务端入口)和 .output/public/(静态资源)。构建产物不包含源码和 node_modules,Nitro 已将所有依赖内联。
6. 使用 PM2 管理
# 安装 PM2
npm install -g pm2
# 启动
pm2 start .output/server/index.mjs --name flutter-api
# 设置开机自启
pm2 startup
pm2 savePM2 Ecosystem 配置
推荐使用配置文件而非命令行参数:
// ecosystem.config.js
module.exports = {
apps: [{
name: 'flutter-api',
script: '.output/server/index.mjs',
instances: 'max', // 根据 CPU 核心数启动多个实例
exec_mode: 'cluster', // 集群模式(零停机重启)
env: {
NODE_ENV: 'production',
HOST: '0.0.0.0',
PORT: 3000,
},
max_memory_restart: '512M', // 内存超限自动重启
error_file: '/var/log/flutter-api/error.log',
out_file: '/var/log/flutter-api/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss',
merge_logs: true,
autorestart: true,
max_restarts: 10, // 1 分钟内重启超过 10 次则停止
restart_delay: 4000, // 重启间隔 4 秒
}],
}# 使用配置文件启动
pm2 start ecosystem.config.js
# 零停机重启
pm2 reload flutter-apiPM2 常用命令
pm2 list # 查看进程
pm2 logs flutter-api # 查看日志
pm2 restart flutter-api # 重启(有短暂中断)
pm2 reload flutter-api # 零停机重启(集群模式)
pm2 stop flutter-api # 停止
pm2 delete flutter-api # 删除
pm2 monit # 实时监控(CPU/内存)
pm2 describe flutter-api # 详细信息restart vs reload
| 命令 | 行为 | 停机时间 | 适用场景 |
|---|---|---|---|
pm2 restart | 杀掉所有实例,再启动 | 有(几秒) | 紧急修复、配置变更 |
pm2 reload | 逐个重启实例 | 零 | 常规更新、代码部署 |
reload 需要集群模式
instances: 'max' + exec_mode: 'cluster'。单实例模式下 reload 等同于 restart。
Nginx 反向代理
# /etc/nginx/sites-available/flutter-api
server {
listen 80;
server_name api.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
# 安全头
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# 代理到 Node.js
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
# 静态资源缓存
location /_nuxt/ {
proxy_pass http://127.0.0.1:3000;
expires 1y;
add_header Cache-Control "public, immutable";
}
# 上传文件大小限制
client_max_body_size 10M;
}# 启用配置
sudo ln -s /etc/nginx/sites-available/flutter-api /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxPM2 集群模式 + Nginx
PM2 集群模式在单机上启动多个 Node.js 进程(通常等于 CPU 核心数),Nginx 代理到同一个端口。PM2 内部负责负载均衡,Nginx 不需要配置 upstream。
Nginx :443 → 127.0.0.1:3000
↓
PM2 (cluster mode, 4 instances)
├─ Worker 0 (PID 1001)
├─ Worker 1 (PID 1002)
├─ Worker 2 (PID 1003)
└─ Worker 3 (PID 1004)SSL 证书
# 安装 Certbot
sudo apt install certbot python3-certbot-nginx
# 获取证书
sudo certbot --nginx -d api.example.com
# 自动续期
sudo certbot renew --dry-runCertbot 自动续期
certbot 安装后会自动添加定时任务,证书到期前自动续期。--dry-run 只是测试续期流程,不会真正续期。
更新部署
#!/bin/bash
# deploy.sh
set -e # 任何命令失败立即退出
cd /app/flutter-api-server
# 1. 拉取最新代码
git pull origin main
# 2. 安装依赖
npm ci
# 3. 数据库迁移(如果有新的迁移)
npx drizzle-kit migrate
# 4. 构建
npm run build
# 5. 零停机重启
pm2 reload flutter-api
echo "部署完成: $(date)"chmod +x deploy.sh
./deploy.sh部署脚本安全改进
#!/bin/bash
set -e
cd /app/flutter-api-server
# 备份数据库
pg_dump -U myuser flutter_api | gzip > /backups/pre-deploy-$(date +%Y%m%d%H%M%S).sql.gz
# 拉取代码
git pull origin main
# 安装依赖 + 构建
npm ci
npm run build
# 健康检查函数
health_check() {
for i in {1..10}; do
if curl -sf http://localhost:3000/health | grep -q '"ok"'; then
echo "健康检查通过"
return 0
fi
sleep 2
done
echo "❌ 健康检查失败,回滚中..."
pm2 restart flutter-api # 回滚到旧版本
return 1
}
# 重启并验证
pm2 reload flutter-api
if ! health_check; then
echo "部署失败!"
exit 1
fi
echo "✅ 部署成功: $(date)"为什么部署后要健康检查?
pm2 reload 成功只代表进程启动了,不代表应用正常。数据库迁移失败、环境变量缺失等问题只有在请求时才会暴露。健康检查确保应用真正可用。
服务器安全加固
# 1. 禁用 root 登录
sudo passwd -l root
# 2. 禁用密码登录(只用 SSH 密钥)
# /etc/ssh/sshd_config
PasswordAuthentication no
PermitRootLogin no
# 3. 配置防火墙
sudo ufw allow 22/tcp # SSH
sudo ufw allow 80/tcp # HTTP
sudo ufw allow 443/tcp # HTTPS
sudo ufw enable
# 4. 自动安全更新
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades不要开放 3000 端口
Nuxt 应用通过 Nginx 反向代理访问,用户不需要直接访问 3000 端口。只开放 80 和 443。
常见问题
PM2 启动后立即崩溃?
# 查看错误日志
pm2 logs flutter-api --err
# 常见原因
# 1. .env 文件缺失或变量名错误
# 2. 数据库连接失败
# 3. 端口被占用构建后运行报 Cannot find module?
Nitro 构建时应该将所有依赖内联到 .output/server/index.mjs。如果缺失:
- 检查
.output/server/index.mjs是否存在 - 确认
npm run build没有报错 - 某些原生模块(如
bcrypt)可能需要特殊配置
内存持续增长?
Node.js 的内存泄漏通常由闭包或事件监听器未清理导致:
# 监控内存
pm2 monit
# 设置内存上限自动重启
# ecosystem.config.js
max_memory_restart: '512M'max_memory_restart 不是解决方案
只是兜底。找到泄漏原因才是根本。