Skip to content
第 25 章 后端 ⏱ 10 分钟阅读

第 25 章:部署与进阶 ​

学习目标 ​

  • 写生产级 Dockerfile(多阶段 + distroless)
  • 用 docker-compose 组合依赖
  • K8s Deployment + Service + Ingress
  • GitHub Actions 自动部署

不写 PM2 / 裸 Node 部署 —— 生产主流是 Docker + K8s。

一、生产构建 ​

bash
pnpm run build                          # 输出 dist/
node dist/main.js

package.json 标准脚本:

json
{
  "scripts": {
    "build": "nest build",
    "start:prod": "node dist/main.js",
    "start:dev": "nest start --watch",
    "lint": "eslint \"src/**/*.ts\"",
    "test": "jest"
  }
}

二、Dockerfile(多阶段) ​

先看几个关键命令。


addgroup / adduser 命令拆解:

bash
RUN addgroup -g 1001 -S nodejs && adduser -S nestjs -u 1001

分两半:

bash
# 第一半:建"部门"
addgroup -g 1001 -S nodejs
 │   │    │   │  │
 │   │    │   └── 部门名 "nodejs"
 │   │    └── -S = 系统组(纯内部用)
 │   └─ gid = 组编号
 └─ 整个 = 建组

# 第二半:招"员工"
adduser -S nestjs -u 1001
 │     │  │      │  │
 │     │  │      └── uid = 员工编号
 │     │  └── 员工名
 │     └─ -S = 系统用户
 └─ 整个 = 建用户

# && = 第一半成功了再跑第二半

类比:公司 = 部门 + 员工

公司:
  - 部门 "nodejs"(编号 1001)      ← addgroup
  - 员工 "nestjs"(编号 1001)      ← adduser
    └─ 员工归属"nodejs"部门

可变的 vs 固定的:

部分类型怎么改
-g 1001(gid)✅ 可变≥1000 的数字,惯例用 1001
-S❌ 固定系统用户标志,不能省
nodejs(组名)✅ 可变跟项目名
nestjs(用户名)✅ 可变跟项目名
-u 1001(uid)✅ 可变≥1000 的数字,跟 gid 对齐

常见写法:

dockerfile
# 写法 1:用户名 = 组名(简单)
addgroup -g 1001 -S app && adduser -S app -u 1001

# 写法 2:不同名字
addgroup -g 1001 -S appgroup && adduser -S appuser -u 1001

惯例:用户/组名跟项目名(避免 nestjs / nodejs / myapp 混淆),uid/gid 固定 1001(避开 0~999 系统保留段)。

生产级多阶段构建:

dockerfile
# ========== 阶段 1:构建 ==========
FROM node:20-alpine AS builder
WORKDIR /app

# ① 先单独装依赖(充分利用缓存层)
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm i --frozen-lockfile

# ② 复制源码编译
COPY . .
RUN pnpm run build && pnpm prune --prod

# ========== 阶段 2:运行 ==========
FROM node:20-alpine
WORKDIR /app

# ③ 只拷贝生产产物 + 依赖
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./

# ④ 安全:用非 root 用户
RUN addgroup -g 1001 -S nodejs && adduser -S nestjs -u 1001
USER nestjs

EXPOSE 3000

# ⑤ 健康检查
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD wget --quiet --tries=1 --spider http://localhost:3000/health || exit 1

CMD ["node", "dist/main.js"]

关键点:

步骤为什么
COPY package.json 先于 .装依赖单独一层,源码变了缓存不失效
--frozen-lockfile锁版本,避免 CI 装到不一致版本
pnpm prune --prod只留生产依赖(devDependencies 拿掉)
USER nestjs非 root 运行(防容器逃逸)
HEALTHCHECKDocker 健康检查,挂了自动重启

⚠️ 坑 1:COPY . . 后才装依赖 → 改一行代码依赖层也失效,构建巨慢。


层缓存机制(为什么 COPY package.json 要先单独写):

Docker 构建每次都跑所有命令,但比对输入没变就复用缓存:

首次构建:
  COPY package.json → 复制 → 生成"层 1"
  RUN pnpm i         → 装依赖 → 生成"层 2"
  COPY . .           → 复制源码 → 生成"层 3"
  RUN pnpm build     → 编译   → 生成"层 4"

改一行 src/main.ts,再次构建:
  COPY package.json → 文件没变 → 跳过 ✅
  RUN pnpm i         → 输入没变 → 跳过 ✅ (不重装依赖)
  COPY . .           → 文件变了 → 重做
  RUN pnpm build     → 输入变了 → 重做
                            ↑
                     只重做后 2 步,~30 秒

对比两种写法:

dockerfile
# A:分离(✅ 推荐)                # B:一起(❌ 慢)
COPY package.json ./              COPY . .
RUN pnpm i --frozen-lockfile      RUN pnpm i
COPY . .                          RUN pnpm build
RUN pnpm build
写法装依赖编译改代码后耗时
A跳过 ✅重做~30 秒
B每次重装重做~5 分钟

pnpm-lock.yaml 也一起 COPY:--frozen-lockfile 锁版本,防止装到不一致的版本。

package.json     → "pnpm@^9"(可能装 9.5 或 9.6)
pnpm-lock.yaml   → "必须装 9.5.1"(精确版本)
--frozen-lockfile → "严格按 lock 装,不要自作主张更新"

三、docker-compose(本地 + 小生产) ​

yaml
# docker-compose.yml
version: '3.9'

services:
  api:
    build: .
    image: my-app:latest
    ports: ['3000:3000']
    env_file: .env.prod
    depends_on:
      db: { condition: service_healthy }    # 等 DB 健康才起
      redis: { condition: service_healthy }
    restart: unless-stopped
    deploy:
      resources:
        limits: { cpus: '1.0', memory: 512M }

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: ${DB_NAME}
    volumes: ['pgdata:/var/lib/postgresql/data']
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U ${DB_USER}']
      interval: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD}
    healthcheck:
      test: ['CMD', 'redis-cli', '-a', '${REDIS_PASSWORD}', 'ping']
      interval: 5s

volumes:
  pgdata:
bash
docker compose up -d
docker compose logs -f api
docker compose ps                          # 看健康状态

四、环境变量 & 密钥管理 ​

不要写明文 .env 到镜像里。生产用:

方案适用
K8s Secret主流
Vault / AWS Secrets Manager大型项目
Sealed Secrets(K8s)GitOps 友好
bash
# K8s Secret(不进 git)
kubectl create secret generic my-app-secret \
  --from-literal=DB_PASSWORD=xxx \
  --from-literal=JWT_SECRET=xxx
yaml
# deployment.yaml
spec:
  containers:
    - name: api
      envFrom:
        - secretRef:
            name: my-app-secret

⚠️ 坑 2:.env 进 git → 密钥泄露,Github 会自动扫描报警。

五、K8s 部署(生产事实标准) ​

5.1 Deployment ​

yaml
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3                                         # 3 副本(高可用)
  selector:
    matchLabels: { app: my-app }
  template:
    metadata:
      labels: { app: my-app }
    spec:
      containers:
        - name: api
          image: my-app:v1.0.0
          ports: [{ containerPort: 3000 }]
          resources:
            requests: { cpu: '100m', memory: 128Mi }  # 最小
            limits:   { cpu: '500m', memory: 512Mi }  # 最大
          readinessProbe:                             # 通过流量进来
            httpGet: { path: /health/readiness, port: 3000 }
            initialDelaySeconds: 10
            periodSeconds: 5
          livenessProbe:                              # 挂了重启
            httpGet: { path: /health/liveness, port: 3000 }
            initialDelaySeconds: 30
            periodSeconds: 10
            failureThreshold: 3
      terminationGracePeriodSeconds: 30               # 宽限期

5.2 Service + Ingress ​

yaml
# service.yaml
apiVersion: v1
kind: Service
metadata: { name: my-app }
spec:
  selector: { app: my-app }
  ports: [{ port: 80, targetPort: 3000 }]
yaml
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: { name: my-app-ingress }
spec:
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service: { name: my-app, port: { number: 80 } }

liveness vs readiness:

探针作用失败时
livenessProbe"进程还活着?"重启 Pod
readinessProbe"能接请求?"从 Service Endpoints 移除流量

5.3 优雅停机 ​

typescript
// main.ts
const app = await NestFactory.create(AppModule);
app.enableShutdownHooks();                            // 监听 SIGTERM
await app.listen(3000);

K8s 发 SIGTERM → NestJS 处理完现有请求 → 退出(最多等 terminationGracePeriodSeconds)。

⚠️ 坑 3:没 enableShutdownHooks() → K8s 强杀时丢请求。

六、CI/CD(GitHub Actions) ​

推荐流程:测试 → 构建镜像 → 推镜像仓库 → K8s 滚动更新。

yaml
# .github/workflows/deploy.yml
name: build-and-deploy
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with: { version: 9 }

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - run: pnpm i --frozen-lockfile
      - run: pnpm run lint
      - run: pnpm run test                             # ⚠️ 测试必须跑
      - run: pnpm run build

      # 镜像推私有仓库
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: registry.cn-hangzhou.aliyuncs.com
          username: ${{ secrets.REGISTRY_USER }}
          password: ${{ secrets.REGISTRY_PASSWORD }}
      - uses: docker/build-push-action@v5
        with:
          push: true
          tags: registry.cn-hangzhou.aliyuncs.com/myteam/my-app:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      # 部署到 K8s(用 kubectl 或 ArgoCD)
      - name: Deploy to K8s
        run: |
          echo "${{ secrets.KUBE_CONFIG }}" > /tmp/kubeconfig
          KUBECONFIG=/tmp/kubeconfig kubectl set image deployment/my-app api=registry.cn-hangzhou.aliyuncs.com/myteam/my-app:${{ github.sha }}

GitOps 流派(更现代):

Git push → CI 构建镜像
                ↓
         ArgoCD 检测到镜像版本变化
                ↓
         自动 sync 到 K8s(自动滚动更新)

⚠️ 坑 4:CI 跳测试直接构建 → 生产出 bug,事故恢复成本高。

七、生产性能调优 ​

项建议
平台@nestjs/platform-fastify(比 Express 快 2~3 倍)
镜像alpine(小)/ distroless(更安全)
进程不开 cluster,让 K8s 跑多副本
缓存Redis(见第 23 章)
压缩Nginx 层 gzip on(Node 不必)
静态资源CDN,不经过 Node
数据库连接池 + 慢查询监控
typescript
// 用 Fastify 平台(项目级切换)
import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';

const app = await NestFactory.create<NestFastifyApplication>(
  AppModule,
  new FastifyAdapter(),
);

八、安全:HTTPS + CORS + Helmet ​

typescript
// main.ts
const app = await NestFactory.create(AppModule);

// CORS(限定可信域名)
app.enableCors({
  origin: ['https://app.example.com'],
  credentials: true,
});

// 安全头
import helmet from 'helmet';
app.use(helmet());

// 限速(防刷)
import rateLimit from 'express-rate-limit';
app.use(rateLimit({ windowMs: 60_000, max: 100 }));    // 1 分钟 100 次

HTTPS 由 K8s Ingress + cert-manager 自动管(Let's Encrypt):

yaml
metadata:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
    - hosts: [api.example.com]
      secretName: my-app-tls

九、监控告警(三件套) ​

见第 24 章。重点指标配置:

yaml
# prometheus-alert.yml(节选)
groups:
  - name: my-app
    rules:
      - alert: HighErrorRate
        expr: rate(http_requests_total{status=~"5.."}[5m]) > 0.01
        for: 5m
        annotations:
          summary: "错误率 >1% 持续 5 分钟"
      - alert: HighLatency
        expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 1
        for: 5m

告警通过 Alertmanager 推到 飞书 / 钉钉 / Slack。

十、本章小结 ​

步骤生产主流
构建多阶段 Dockerfile + distroless
部署K8s(不用 PM2、不用裸 Node)
反代Nginx Ingress + 自动 HTTPS
CI/CDGitHub Actions + 镜像仓库 + K8s
密钥K8s Secret / Vault(不入 git)
监控Prometheus + Grafana + Sentry
平台Fastify(快 2~3 倍)
不写PM2 / 裸 Node / Express(默认)

生产部署黄金标准:

Git push → CI 测试 → 构建镜像 → 推镜像
                                ↓
                          K8s 滚动更新
                                ↓
                          流量切到新 Pod
                                ↓
                       Prometheus + Grafana 监控

教程结束。NestJS 是 Node 生态最工程化的框架,继续深入方向:K8s Operator、Serverless(Lambda)、GraphQL、可观测性(SLO/SLI)。

本站基于 VitePress 构建 · 由 StackHub 团队维护