第 25 章:部署与进阶
学习目标
- 写生产级 Dockerfile(多阶段 + distroless)
- 用 docker-compose 组合依赖
- K8s Deployment + Service + Ingress
- GitHub Actions 自动部署
不写 PM2 / 裸 Node 部署 —— 生产主流是 Docker + K8s。
一、生产构建
pnpm run build # 输出 dist/
node dist/main.jspackage.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 命令拆解:
RUN addgroup -g 1001 -S nodejs && adduser -S nestjs -u 1001分两半:
# 第一半:建"部门"
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 对齐 |
常见写法:
# 写法 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 系统保留段)。
生产级多阶段构建:
# ========== 阶段 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 运行(防容器逃逸) |
HEALTHCHECK | Docker 健康检查,挂了自动重启 |
⚠️ 坑 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 秒对比两种写法:
# 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(本地 + 小生产)
# 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:docker compose up -d
docker compose logs -f api
docker compose ps # 看健康状态四、环境变量 & 密钥管理
不要写明文 .env 到镜像里。生产用:
| 方案 | 适用 |
|---|---|
| K8s Secret | 主流 |
| Vault / AWS Secrets Manager | 大型项目 |
| Sealed Secrets(K8s) | GitOps 友好 |
# K8s Secret(不进 git)
kubectl create secret generic my-app-secret \
--from-literal=DB_PASSWORD=xxx \
--from-literal=JWT_SECRET=xxx# deployment.yaml
spec:
containers:
- name: api
envFrom:
- secretRef:
name: my-app-secret⚠️ 坑 2:
.env进 git → 密钥泄露,Github 会自动扫描报警。
五、K8s 部署(生产事实标准)
5.1 Deployment
# 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
# service.yaml
apiVersion: v1
kind: Service
metadata: { name: my-app }
spec:
selector: { app: my-app }
ports: [{ port: 80, targetPort: 3000 }]# 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 优雅停机
// 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 滚动更新。
# .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 |
| 数据库 | 连接池 + 慢查询监控 |
// 用 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
// 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):
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts: [api.example.com]
secretName: my-app-tls九、监控告警(三件套)
见第 24 章。重点指标配置:
# 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/CD | GitHub 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)。