部署与运维

让应用在互联网上稳定运行

🎯 学习目标

学完本章,你将能够:

  • 将应用部署到 Vercel、Railway 等 PaaS 平台
  • 管理多环境配置和启动时环境变量校验
  • 使用 Pino 实现结构化日志记录
  • 配置 PM2 进程管理和 Nginx 反向代理
  • 集成 Sentry 错误追踪和健康检查端点

你有没有想过

写完代码、跑通测试之后,怎么让全世界都能访问你的网站?服务器挂了怎么办?数据库数据丢了怎么办?

部署不是简单地把文件传到服务器。生产环境需要:进程管理、反向代理、SSL 证书、数据库备份、监控告警。

这一章覆盖从 PaaS 快速部署到传统 VPS 运维的完整知识链。

PaaS 平台对比

三个最流行的部署平台

特性VercelRailwayRender
最适合Next.js / 前端全栈 + 数据库Web 服务 + Worker
数据库需外部服务内置 PG/Redis内置 PG/Redis
自动部署Git push 自动Git push 自动Git push 自动
预览部署每个 PR 自动支持支持
Serverless核心特性不支持部分支持
WebSocket有限支持支持

Part 1: PaaS 部署

推代码即部署,适合快速上线。

部署到 Vercel

两条命令上线你的 Next.js 应用

$ npm install -g vercel
$ vercel login
$ vercel                    # 首次部署(preview)
$ vercel --prod             # 生产部署
// vercel.json(可选配置)
{
  "framework": "nextjs",
  "env": {
    "DATABASE_URL": "@database_url",
    "JWT_SECRET": "@jwt_secret"
  }
}
$ vercel env add DATABASE_URL production
$ vercel env add JWT_SECRET production

部署到 Railway

内置数据库,全栈一站搞定

$ npm install -g @railway/cli
$ railway login
$ railway init
$ railway up                    # 部署
$ railway run npm run migrate   # 在 Railway 环境运行迁移
# railway.toml(可选配置)
[build]
buildCommand = "npm run build"

[deploy]
startCommand = "node dist/index.js"
healthcheckPath = "/health"
restartPolicyType = "on_failure"

Part 2: 环境管理

"怎么跑"与"在哪儿跑"分开 —— 同一份代码,不同环境不同配置。

环境变量管理

development / staging / production 各司其职

环境用途数据来源
development本地开发.env.local(不入库)
staging预发测试平台 UI / Vault
production线上对外平台 UI / Secret(加密)

代码与环境解耦:同一份构建产物在不同环境注入不同的环境变量即可运行。本地用 .env 文件;线上优先使用平台提供的 Secret 管理(Vercel Env、Railway Variables、Vault 等),千万不要把 .env 文件上传到服务器。

# 本地开发:创建 .env(确保被 .gitignore 排除)
DATABASE_URL=postgresql://user:pass@localhost:5432/devdb
JWT_SECRET=dev-only-secret-change-me
REDIS_URL=redis://localhost:6379
LOG_LEVEL=debug

# .env.example 用作模板提交到仓库,真实值留空
DATABASE_URL=
JWT_SECRET=

启动时校验环境变量

dotenv + zod —— 少一个变量立刻崩,比线上 500 强

运行时才发现的配置错误代价高昂。用 TypeScript + Zod 在入口文件最前面做一次 schema 校验;缺字段、类型错、端口越界都立刻报错退出。

// src/config/env.ts
import { z } from 'zod';
import dotenv from 'dotenv';

// 开发环境读取 .env;线上由平台注入环境变量,不读文件
if (process.env.NODE_ENV === 'development') {
  dotenv.config({ path: '.env.local' });
}

const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'staging', 'production'])
    .default('development'),
  PORT: z.coerce.number().int().positive().default(3000),
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32, 'secret too short'),
  REDIS_URL: z.string().url().optional(),
  LOG_LEVEL: z.enum(['trace','debug','info','warn','error'])
    .default('info'),
});

const parsed = envSchema.safeParse(process.env);

if (!parsed.success) {
  // 启动时直接失败——比线上 500 好得多
  console.error(
    'Invalid env:\n',
    JSON.stringify(parsed.error.flatten().fieldErrors, null, 2)
  );
  process.exit(1);
}

export const env = parsed.data;

数据库连接池配置

连接数不是越多越好 —— 按 CPU 核数和实例数反推

每个 Node.js 实例与 PostgreSQL 之间维持一个连接池。连接数过少会排队等待;过多则浪费数据库资源甚至被打满。经验公式:pool_size = (CPU核数 * 2) + 有效磁盘数,再除以实例数。

// Prisma schema (prisma/schema.prisma)
        datasource db {
          provider = "postgresql"
          url      = env("DATABASE_URL")
          // 连接池大小通过 URL 参数传递
        }
        
        // .env
        // 单实例 4 核:pool_size=10
        DATABASE_URL="postgresql://user:pass@host:5432/mydb?connection_limit=10&pool_timeout=30"
        
        // 多实例(PM2 cluster 4 workers):每个 worker 5 个连接
        DATABASE_URL="postgresql://user:pass@host:5432/mydb?connection_limit=5&pool_timeout=30"
# 查看当前 PG 连接数
        $ psql -c "SELECT count(*) FROM pg_stat_activity WHERE datname='mydb';"
        # 查看最大连接数(默认 100)
        $ psql -c "SHOW max_connections;"

日志管理:别再用 console.log

结构化日志 + 分级输出,定位问题快十倍

console.log 输出的是纯文本字符串,grep 不到字段、过滤不了级别、stdout 缓冲还可能丢日志。生产环境推荐 JSON 结构化日志,能被 Loki / ELK / Datadog 直接采集,按 serviceName、level、traceId 聚合查询。

级别使用场景
trace进出函数、极其详细
debug本地排查,线上关闭
info启动完成、关键业务节点
warn降级、重试、可恢复异常
error不可恢复错误,需要介入
# .env(示例)
LOG_LEVEL=info          # 生产
LOG_LEVEL=debug         # 本地

Pino:最快的 Node.js 日志库

低开销 + 自带 Express 中间件 pino-http

Pino 把序列化推迟到 Worker 线程,单线程每秒可写数十万条日志。pino-http 自动给每个请求打一条 info 日志(method、url、status、响应时间),并提供绑定当前请求上下文的子 logger。

$ npm install pino pino-http
$ npm install -D pino-pretty     # 开发时用,生产不装
// src/logger.ts
import pino from 'pino';
import { env } from './config/env';

export const logger = pino({
  level: env.LOG_LEVEL,
  // 开发环境:彩色、可读输出;生产环境:JSON 到 stdout
  transport: env.NODE_ENV === 'development'
    ? { target: 'pino-pretty', options: { colorize: true, translateTime: 'HH:SS' } }
    : undefined,
  base: {
    service: 'my-app',
    version: process.env.npm_package_version ?? '0.0.0',
  },
});
// src/index.ts —— 接入 Express 请求日志
import express from 'express';
import pinoHttp from 'pino-http';
import { logger } from './logger';

const app = express();

// 每个请求自动打 info 日志:method / url / status / 耗时
app.use(pinoHttp({ logger }));

app.get('/api/users/:id', async (req, res) => {
  // req.log 是绑定当前请求上下文的子 logger
  req.log.info({ userId: req.params.id }, 'fetching user');
  const user = await getUser(req.params.id);
  res.json(user);
});

Part 5: 传统 VPS

PM2 + Nginx + SSL,完全掌控你的服务器。

VPS 部署架构

用户请求从互联网到 Node.js 的完整链路

图:传统 VPS 部署的完整架构图

PM2 进程管理

Node.js 的守护神

$ npm install -g pm2
$ pm2 start dist/index.js --name "my-app"
// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'my-app',
    script: 'dist/index.js',
    instances: 'max',           // 按 CPU 核心数启动
    exec_mode: 'cluster',       // 集群模式
    max_memory_restart: '512M', // 内存超限自动重启
    env_production: { NODE_ENV: 'production', PORT: 3000 },
  }],
};
$ pm2 start ecosystem.config.js --env production
$ pm2 list          # 查看进程
$ pm2 logs my-app   # 查看日志
$ pm2 monit         # 实时监控
$ pm2 reload my-app # 零停机重载
$ pm2 startup       # 开机自启
$ pm2 save          # 保存进程列表

Nginx 反向代理

HTTPS、安全头、静态文件一站搞定

# /etc/nginx/sites-available/my-app
upstream nodejs_app {
    server 127.0.0.1:3000;
    keepalive 64;
}

server {
    listen 443 ssl http2;
    server_name myapp.com;

    ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem;

    # 安全头
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;

    # API 反向代理
    location /api/ {
        proxy_pass http://nodejs_app;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

    location / {
        proxy_pass http://nodejs_app;
    }
}

SSL 证书

Let's Encrypt 免费 HTTPS

# 安装 Certbot
$ sudo apt-get install -y certbot python3-certbot-nginx

# 获取证书(自动配置 Nginx)
$ sudo certbot --nginx -d myapp.com -d www.myapp.com

# 自动续期(Certbot 已自动添加定时任务)
$ sudo certbot renew --dry-run
确保 Nginx 不暴露敏感路径。显式拒绝 /.env、/node_modules/、/.git/ 的外部访问。

Part 4: 监控与日志

知道出问题了,才能解决问题。

健康检查端点

让监控系统知道你的应用还好不好

// src/routes/health.ts
app.get('/health', async (req, res) => {
  const checks = {
    status: 'ok',
    uptime: process.uptime(),
    checks: {
      database: await checkDatabase(),
      redis: await checkRedis(),
      memory: checkMemory(),
    },
  };
  const isHealthy = Object.values(checks.checks)
    .every(c => c.status === 'ok');
  res.status(isHealthy ? 200 : 503).json(checks);
});

async function checkDatabase() {
  try {
    await prisma.$queryRaw`SELECT 1`;
    return { status: 'ok' };
  } catch (error) {
    return { status: 'error', message: error.message };
  }
}

监控工具选型

从小型项目到企业级,按需选择

层级工具监控内容
可用性UptimeRobot / Uptime Kuma服务是否在线
指标Prometheus + GrafanaCPU/内存/请求量
错误Sentry异常堆栈、用户影响
日志Pino + ELK / Loki结构化日志

小型项目推荐免费组合:UptimeRobot + Sentry + PM2。规模增长后再引入 Prometheus + Grafana。

Sentry 错误追踪

线上报错了,第一时间知道

// npm install @sentry/node
import * as Sentry from '@sentry/node';

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
  tracesSampleRate: 0.1,
});

// Express 中间件(放在路由前后)
app.use(Sentry.Handlers.requestHandler());
// ... 你的路由 ...
app.use(Sentry.Handlers.errorHandler());

// 手动捕获错误
try {
  await riskyOperation();
} catch (error) {
  Sentry.captureException(error, {
    extra: { userId: currentUser.id },
  });
}

Part 3: 进阶运维

零停机、全球加速、可灰度 —— 让部署不再是"心惊胆战"的时刻。

部署策略对比

滚动更新 / 蓝绿部署 / 金丝雀发布

不同业务场景需要不同的部署策略。小型项目用 PM2 reload 就够了;中大型系统需要更精细的流量控制。

策略原理优点缺点
滚动替换逐台重启 worker简单、资源少版本短暂共存
蓝绿部署两套环境切流量秒级回滚资源翻倍
金丝雀发布先放 5% 流量风险可控需要网关支持
# 蓝绿部署示意(Nginx 切换 upstream)
        # 当前:green 在跑
        $ ln -sfn /var/www/green /var/www/current
        $ nginx -s reload
        
        # 出问题:秒级切回 blue
        $ ln -sfn /var/www/blue /var/www/current
        $ nginx -s reload
        
        # 金丝雀:Nginx split_clients 按比例分流
        split_clients "${remote_addr}${http_user_agent}" $app_variant {
            5%     canary;
            *      stable;
        }

零停机部署

pm2 reload 热重载 vs pm2 restart 冷重启

restart 先杀后起,期间所有请求失败;reload 会先启动新 worker,等其 ready 后再优雅关掉旧 worker —— 整个过程服务不中断。前提是应用必须支持优雅关闭(Graceful Shutdown)。

命令行为影响
pm2 restart杀进程再启所有在途请求失败
pm2 reload滚动替换 worker对新请求零影响
pm2 gracefulReload同 reload,走 IPC同上
# ecosystem.config.js —— 关键:cluster 模式 + wait_ready
module.exports = {
  apps: [{
    name: 'my-app',
    script: 'dist/index.js',
    instances: 'max',          // 按 CPU 核数起多个 worker
    exec_mode: 'cluster',
    wait_ready: true,          // 等 process.send('ready') 再视为启动完成
    listen_timeout: 8000,      // 8 秒未 ready 视为失败
    kill_timeout: 5000,        // 给旧 worker 5 秒优雅关闭
    max_memory_restart: '500M',
  }],
};


        
        

优雅关闭(Graceful Shutdown)

收到 SIGTERM 后:停止接收新请求 → 处理完在途请求 → 关闭连接 → 退出

零停机部署的前提是应用能正确处理 SIGTERM。PM2 reload 发出 SIGTERM 后等待 kill_timeout,超时则强杀。应用必须自己关闭 HTTP server、数据库连接、消息队列消费者。

// src/graceful-shutdown.ts
        import { Server } from 'http';
        import { prisma } from './db';
        import { logger } from './logger';
        
        export function setupGracefulShutdown(server: Server) {
          let isShuttingDown = false;
        
          const shutdown = async (signal: string) => {
            if (isShuttingDown) return;
            isShuttingDown = true;
            logger.info({ signal }, 'shutdown initiated');
        
            // 1. 停止接收新请求
            server.close((err) => {
              if (err) logger.error({ err }, 'error closing server');
              else logger.info('http server closed');
            });
        
            // 2. 关闭数据库连接
            await prisma.$disconnect();
            logger.info('database disconnected');
        
            // 3. 通知 PM2 已就绪退出(配合 wait_ready)
            process.exit(0);
          };
        
          process.on('SIGTERM', () => shutdown('SIGTERM'));
          process.on('SIGINT',  () => shutdown('SIGINT'));
        }
        
        // src/index.ts
        import { setupGracefulShutdown } from './graceful-shutdown';
        const server = app.listen(env.PORT, () => {
          logger.info(`listening on :${env.PORT}`);
          process.send?.('ready');   // 通知 PM2:我已就绪
        });
        setupGracefulShutdown(server);
#!/usr/bin/env bash # deploy.sh —— 零停机一键部署 set -euo pipefail echo '⬇️ pulling code...' git pull origin main echo '📦 installing deps...' npm ci --omit=dev echo '🔨 building...' npm run build echo '🗃️ running migrations...' npm run migrate echo '🔄 reloading PM2 (zero-downtime)...' pm2 reload ecosystem.config.js --update-env echo '✅ deploy done'

Nginx upstream 滚动更新

多实例场景下,逐台摘流量、更新、加回

当应用跑在多台服务器或同一台多个端口上,Nginx 的 upstream 配合健康检查可以实现逐台滚动更新,比单机 PM2 reload 更可控。

# /etc/nginx/conf.d/upstream.conf
upstream node_backend {
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
    server 127.0.0.1:3003;
}

server {
    listen 80;
    location / {
        proxy_pass http://node_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_next_upstream error timeout http_502;
    }
}
# 滚动更新单台:先标记 down,更新完再恢复
curl -X POST 'http://nginx-api/servers/3001/down'
pm2 reload my-app --only 3001
sleep 5
curl -X POST 'http://nginx-api/servers/3001/up'

CDN 与静态资源加速

把"离用户最近"这件事交给专业服务商

CDN(Content Delivery Network)在全球各地部署边缘节点,把静态资源缓存到离用户最近的位置。用户请求不再跨洋回源,延迟从 200ms 降到 20ms。

图:CDN 工作原理——全球边缘节点加速
CDN 服务免费额度特点
Cloudflare无限流量(免费套餐)自带 DDoS 防护、Workers
AWS CloudFront1TB / 月(首年)与 S3 / EC2 深度集成
Vercel Edge100GB 带宽Next.js 原生支持
阿里云 DCDN按量付费国内节点多、ICP 支持

静态资源长期缓存

文件名带 hash,缓存一年也不怕

构建工具(Vite / Webpack / Next.js)会给静态资源生成内容 hash —— 文件内容变了,文件名就变了。这样可以对静态资源设置 Cache-Control: max-age=31536000, immutable,大幅提升二次访问速度。

# 构建产物示例
assets/
  index-a1b2c3d4.js        # 内容变了 → hash 变了 → URL 变了
  vendor-e5f6g7h8.js
  style.i9j0k1l2.css
# Nginx 配置:静态资源长期缓存,HTML 不缓存
server {
    location /assets/ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    location / {
        expires -1;   # HTML 每次都回源
        proxy_pass http://node_backend;
    }
}

故障恢复清单

出问题时按顺序排查,别慌

线上故障第一原则:先恢复,再追因。能回滚就回滚,能重启就重启,保留现场日志留到事后分析。

步骤命令 / 操作目的
1. 确认范围curl -I https://your.app/health全站 or 单接口
2. 看监控Sentry / Grafana / PM2 monit定位异常源
3. 看日志pm2 logs --nostream --lines 200找 stack trace
4. 回滚git revert HEAD && pm2 reload all先恢复
5. 复盘写 postmortem,加告警防止复发
# 一键回滚脚本
#!/usr/bin/env bash
set -euo pipefail
echo '⏪ reverting last commit...'
git revert --no-edit HEAD
npm run build
pm2 reload all
revert_exit=$?
if [ $revert_exit -ne 0 ]; then
  echo '❌ reload failed, resetting git...'
  git reset --hard HEAD@{1}
  exit 1
fi
echo '✅ rollback done'

排错:服务挂了

症状:网站突然无法访问,502 Bad Gateway。
原因:Node.js 进程崩溃(内存溢出、未捕获异常)。
修复:PM2 会自动重启。用 pm2 logs 查看崩溃日志,定位原因。常见问题:内存泄漏(重启频率高)、未处理的 Promise rejection。
$ pm2 logs my-app --lines 100   # 查看最近日志
$ pm2 show my-app                # 查看重启次数
$ pm2 monit                      # 实时监控内存和 CPU

试一试

练习一

任务:在你的应用中添加 /health 端点,检查数据库连接和内存使用情况。

参考答案

app.get('/health', async (req, res) => {
  try {
    await prisma.$queryRaw`SELECT 1`;
    const mem = process.memoryUsage();
    res.json({ status: 'ok', heapUsed: mem.heapUsed });
  } catch (e) {
    res.status(503).json({ status: 'error' });
  }
});

练习二

任务:将应用部署到 Vercel 或 Railway,配置环境变量并验证线上可访问。

# 提示:先在平台导入 GitHub 仓库
# 再配置 DATABASE_URL 等环境变量
# 最后访问平台分配的域名验证

练习三

任务:编写一个 bash 脚本 deploy.sh,实现零停机部署:拉代码 → 安装依赖 → 构建 → 迁移 → pm2 reload all。

参考答案

#!/usr/bin/env bash
set -euo pipefail

echo '⬇️  pulling code...'
git pull origin main

echo '📦 installing deps...'
npm ci --omit=dev

echo '🔨 building...'
npm run build

echo '🗃️  running migrations...'
npm run migrate

echo '🔄 reloading all PM2 apps (zero-downtime)...'
pm2 reload all

echo '✅ deploy done'

练习四

任务:配置 Pino,让开发环境输出彩色可读日志,生产环境输出 JSON 到 stdout。

参考答案

import pino from 'pino';

const isDev = process.env.NODE_ENV === 'development';

export const logger = pino({
  level: process.env.LOG_LEVEL ?? (isDev ? 'debug' : 'info'),
  transport: isDev
    ? { target: 'pino-pretty', options: { colorize: true, translateTime: 'HH:SS' } }
    : undefined,
  base: { service: 'my-app' },
});

上线前安全加固清单

别让部署日变成被黑日

部署完不等于结束。下面这些是生产环境最容易忽略的安全项,每条都可能成为攻击入口。

项目做法
Helmet 头app.use(helmet()) —— X-Frame-Options / CSP / HSTS
CORS 白名单只允许业务域名,不要 *
Rate Limitingexpress-rate-limit —— 单 IP 限流
.env 不入库.gitignore + 平台 Secret 管理
依赖漏洞npm audit / GitHub Dependabot
最小权限数据库账号只给必要权限,不要用 superuser
// 最小权限示例:应用账号只读 + 写特定表
// migration 用高权限账号,运行时用受限账号
// DATABASE_URL=migrator:pass@host/db       ← 仅迁移时用
// DATABASE_URL=app_user:pass@host/db       ← 运行时用
// app_user 权限: SELECT/INSERT/UPDATE/DELETE ON app_schema.*

📝 要点总结

本章核心知识点

PaaS 部署

Vercel/Railway 等平台让部署变得简单,专注代码而非基础设施

日志管理

结构化日志 + 级别控制 + 集中收集,快速定位生产问题

进程管理

PM2 提供自动重启、负载均衡和日志管理,生产环境必备

监控告警

健康检查 + Sentry 错误追踪 + 性能指标 = 可观测的生产系统

下一章

应用跑起来了,但用户抱怨慢?下一章是最后一章——性能优化与安全,让应用又快又安全。

1 / X