Skip to content

直接部署

服务器环境准备

安装 Node.js

bash
# 使用 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

bash
# 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

bash
sudo apt install redis-server
sudo systemctl enable redis-server

Redis 安全加固

bash
# /etc/redis/redis.conf
requirepass your-strong-password   # 设置密码
bind 127.0.0.1                     # 只监听本地

为什么 Redis 要设密码?

默认 Redis 无密码且监听所有接口。如果服务器有公网 IP,攻击者可以直接连接 Redis 执行任意命令(如写入 SSH 密钥)。

部署步骤

1. 上传代码

bash
# 方式一: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. 安装依赖

bash
npm ci

npm ci vs npm install

npm ci 严格按照 package-lock.json 安装,确保版本一致。npm install 可能自动升级补丁版本,导致生产环境与开发环境不一致。

3. 配置环境变量

bash
cp .env.example .env
nano .env  # 编辑环境变量

4. 数据库迁移

bash
npx drizzle-kit migrate

迁移前备份数据库!

迁移可能修改表结构,如果出错需要回滚。参考"上线与运维"章节的备份策略。

5. 构建

bash
npm run build

构建输出在 .output/ 目录

nuxt build 生成 .output/server/index.mjs(服务端入口)和 .output/public/(静态资源)。构建产物不包含源码和 node_modules,Nitro 已将所有依赖内联。

6. 使用 PM2 管理

bash
# 安装 PM2
npm install -g pm2

# 启动
pm2 start .output/server/index.mjs --name flutter-api

# 设置开机自启
pm2 startup
pm2 save

PM2 Ecosystem 配置

推荐使用配置文件而非命令行参数:

js
// 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 秒
  }],
}
bash
# 使用配置文件启动
pm2 start ecosystem.config.js

# 零停机重启
pm2 reload flutter-api

PM2 常用命令

bash
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 反向代理

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;
}
bash
# 启用配置
sudo ln -s /etc/nginx/sites-available/flutter-api /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

PM2 集群模式 + Nginx

PM2 集群模式在单机上启动多个 Node.js 进程(通常等于 CPU 核心数),Nginx 代理到同一个端口。PM2 内部负责负载均衡,Nginx 不需要配置 upstream

text
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 证书

bash
# 安装 Certbot
sudo apt install certbot python3-certbot-nginx

# 获取证书
sudo certbot --nginx -d api.example.com

# 自动续期
sudo certbot renew --dry-run

Certbot 自动续期

certbot 安装后会自动添加定时任务,证书到期前自动续期。--dry-run 只是测试续期流程,不会真正续期。

更新部署

bash
#!/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)"
bash
chmod +x deploy.sh
./deploy.sh

部署脚本安全改进

bash
#!/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 成功只代表进程启动了,不代表应用正常。数据库迁移失败、环境变量缺失等问题只有在请求时才会暴露。健康检查确保应用真正可用。

服务器安全加固

bash
# 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 启动后立即崩溃?

bash
# 查看错误日志
pm2 logs flutter-api --err

# 常见原因
# 1. .env 文件缺失或变量名错误
# 2. 数据库连接失败
# 3. 端口被占用

构建后运行报 Cannot find module

Nitro 构建时应该将所有依赖内联到 .output/server/index.mjs。如果缺失:

  1. 检查 .output/server/index.mjs 是否存在
  2. 确认 npm run build 没有报错
  3. 某些原生模块(如 bcrypt)可能需要特殊配置

内存持续增长?

Node.js 的内存泄漏通常由闭包或事件监听器未清理导致:

bash
# 监控内存
pm2 monit

# 设置内存上限自动重启
# ecosystem.config.js
max_memory_restart: '512M'

max_memory_restart 不是解决方案

只是兜底。找到泄漏原因才是根本。

基于 Nuxt 4 官方文档整理编写