814 lines
18 KiB
Markdown
814 lines
18 KiB
Markdown
# Authorization Service 部署文档
|
|
|
|
## 目录
|
|
|
|
1. [部署架构](#部署架构)
|
|
2. [环境配置](#环境配置)
|
|
3. [Docker 部署](#docker-部署)
|
|
4. [Kubernetes 部署](#kubernetes-部署)
|
|
5. [数据库迁移](#数据库迁移)
|
|
6. [监控与日志](#监控与日志)
|
|
7. [故障排除](#故障排除)
|
|
|
|
---
|
|
|
|
## 部署架构
|
|
|
|
### 整体架构
|
|
|
|
```
|
|
┌─────────────────┐
|
|
│ Load Balancer │
|
|
│ (Nginx/ALB) │
|
|
└────────┬────────┘
|
|
│
|
|
┌───────────────────┼───────────────────┐
|
|
│ │ │
|
|
┌─────────▼─────────┐ ┌──────▼──────┐ ┌─────────▼─────────┐
|
|
│ Authorization │ │ Identity │ │ Other │
|
|
│ Service │ │ Service │ │ Services │
|
|
│ (Port 3002) │ │ (Port 3001) │ │ │
|
|
└─────────┬─────────┘ └─────────────┘ └──────────────────┘
|
|
│
|
|
┌─────────┴─────────────────────────────────┐
|
|
│ │
|
|
┌───▼───┐ ┌────────┐ ┌────────┐ ┌──────────┐
|
|
│ DB │ │ Redis │ │ Kafka │ │ External │
|
|
│(PG 15)│ │ (7.x) │ │(3.7.x) │ │ Services │
|
|
└───────┘ └────────┘ └────────┘ └──────────┘
|
|
```
|
|
|
|
### 服务依赖
|
|
|
|
| 依赖 | 版本 | 用途 |
|
|
|------|------|------|
|
|
| PostgreSQL | 15.x | 主数据库 |
|
|
| Redis | 7.x | 缓存、会话 |
|
|
| Kafka | 3.7.x | 事件消息队列 |
|
|
| Identity Service | - | JWT 验证 |
|
|
| Referral Service | - | 推荐关系查询 |
|
|
| Statistics Service | - | 团队统计查询 |
|
|
|
|
---
|
|
|
|
## 环境配置
|
|
|
|
### 环境变量
|
|
|
|
```bash
|
|
# .env.production
|
|
|
|
# 应用配置
|
|
NODE_ENV=production
|
|
PORT=3002
|
|
|
|
# 数据库配置
|
|
DATABASE_URL=postgresql://user:password@db-host:5432/authorization_prod
|
|
|
|
# Redis 配置
|
|
REDIS_HOST=redis-host
|
|
REDIS_PORT=6379
|
|
REDIS_PASSWORD=redis-password
|
|
|
|
# Kafka 配置
|
|
KAFKA_BROKERS=kafka-1:9092,kafka-2:9092,kafka-3:9092
|
|
KAFKA_CLIENT_ID=authorization-service
|
|
KAFKA_CONSUMER_GROUP=authorization-service-group
|
|
|
|
# JWT 配置
|
|
JWT_SECRET=your-production-jwt-secret-key
|
|
JWT_EXPIRES_IN=1h
|
|
|
|
# 外部服务
|
|
IDENTITY_SERVICE_URL=http://identity-service:3001
|
|
REFERRAL_SERVICE_URL=http://referral-service:3003
|
|
STATISTICS_SERVICE_URL=http://statistics-service:3004
|
|
|
|
# 日志
|
|
LOG_LEVEL=info
|
|
```
|
|
|
|
### 配置说明
|
|
|
|
| 配置项 | 说明 | 默认值 |
|
|
|--------|------|--------|
|
|
| NODE_ENV | 运行环境 | production |
|
|
| PORT | 服务端口 | 3002 |
|
|
| DATABASE_URL | PostgreSQL 连接字符串 | - |
|
|
| REDIS_HOST | Redis 主机地址 | localhost |
|
|
| REDIS_PORT | Redis 端口 | 6379 |
|
|
| KAFKA_BROKERS | Kafka broker 地址列表 | - |
|
|
| JWT_SECRET | JWT 签名密钥 | - |
|
|
| LOG_LEVEL | 日志级别 | info |
|
|
|
|
### 密钥管理
|
|
|
|
生产环境建议使用密钥管理服务:
|
|
|
|
- **AWS**: AWS Secrets Manager / Parameter Store
|
|
- **阿里云**: KMS / 密钥管理服务
|
|
- **Kubernetes**: Secrets
|
|
|
|
---
|
|
|
|
## Docker 部署
|
|
|
|
### 生产 Dockerfile
|
|
|
|
```dockerfile
|
|
# Dockerfile
|
|
|
|
# 构建阶段
|
|
FROM node:20-alpine AS builder
|
|
|
|
WORKDIR /app
|
|
|
|
# 安装 OpenSSL
|
|
RUN apk add --no-cache openssl openssl-dev libc6-compat
|
|
|
|
# 复制依赖文件
|
|
COPY package*.json ./
|
|
COPY prisma ./prisma/
|
|
|
|
# 安装依赖
|
|
RUN npm ci --only=production
|
|
|
|
# 生成 Prisma 客户端
|
|
RUN npx prisma generate
|
|
|
|
# 复制源代码
|
|
COPY . .
|
|
|
|
# 构建
|
|
RUN npm run build
|
|
|
|
# 生产阶段
|
|
FROM node:20-alpine AS production
|
|
|
|
WORKDIR /app
|
|
|
|
# 安装运行时依赖
|
|
RUN apk add --no-cache openssl libc6-compat
|
|
|
|
# 复制构建产物
|
|
COPY --from=builder /app/dist ./dist
|
|
COPY --from=builder /app/node_modules ./node_modules
|
|
COPY --from=builder /app/package*.json ./
|
|
COPY --from=builder /app/prisma ./prisma
|
|
|
|
# 创建非 root 用户
|
|
RUN addgroup -g 1001 -S nodejs && \
|
|
adduser -S nestjs -u 1001 && \
|
|
chown -R nestjs:nodejs /app
|
|
|
|
USER nestjs
|
|
|
|
# 健康检查
|
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
|
CMD wget --no-verbose --tries=1 --spider http://localhost:3002/health || exit 1
|
|
|
|
EXPOSE 3002
|
|
|
|
CMD ["node", "dist/main.js"]
|
|
```
|
|
|
|
### Docker Compose (生产)
|
|
|
|
```yaml
|
|
# docker-compose.prod.yml
|
|
|
|
version: '3.8'
|
|
|
|
services:
|
|
authorization-service:
|
|
build:
|
|
context: .
|
|
dockerfile: Dockerfile
|
|
ports:
|
|
- "3002:3002"
|
|
environment:
|
|
- NODE_ENV=production
|
|
- DATABASE_URL=postgresql://postgres:password@db:5432/authorization
|
|
- REDIS_HOST=redis
|
|
- REDIS_PORT=6379
|
|
- KAFKA_BROKERS=kafka:9092
|
|
- JWT_SECRET=${JWT_SECRET}
|
|
depends_on:
|
|
db:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
kafka:
|
|
condition: service_healthy
|
|
restart: unless-stopped
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
cpus: '1'
|
|
memory: 1G
|
|
reservations:
|
|
cpus: '0.5'
|
|
memory: 512M
|
|
|
|
db:
|
|
image: postgres:15-alpine
|
|
environment:
|
|
POSTGRES_USER: postgres
|
|
POSTGRES_PASSWORD: password
|
|
POSTGRES_DB: authorization
|
|
volumes:
|
|
- postgres_data:/var/lib/postgresql/data
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
restart: unless-stopped
|
|
|
|
redis:
|
|
image: redis:7-alpine
|
|
command: redis-server --requirepass ${REDIS_PASSWORD}
|
|
volumes:
|
|
- redis_data:/data
|
|
healthcheck:
|
|
test: ["CMD", "redis-cli", "ping"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
restart: unless-stopped
|
|
|
|
kafka:
|
|
image: apache/kafka:3.7.0
|
|
environment:
|
|
KAFKA_NODE_ID: 1
|
|
KAFKA_PROCESS_ROLES: broker,controller
|
|
KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
|
|
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
|
|
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
|
|
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
|
|
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
|
|
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
|
CLUSTER_ID: MkU3OEVBNTcwNTJENDM2Qk
|
|
volumes:
|
|
- kafka_data:/var/lib/kafka/data
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "/opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --list"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 5
|
|
start_period: 60s
|
|
restart: unless-stopped
|
|
|
|
volumes:
|
|
postgres_data:
|
|
redis_data:
|
|
kafka_data:
|
|
|
|
networks:
|
|
default:
|
|
driver: bridge
|
|
```
|
|
|
|
### 构建和部署
|
|
|
|
```bash
|
|
# 构建镜像
|
|
docker build -t authorization-service:latest .
|
|
|
|
# 推送到镜像仓库
|
|
docker tag authorization-service:latest your-registry/authorization-service:latest
|
|
docker push your-registry/authorization-service:latest
|
|
|
|
# 使用 Docker Compose 部署
|
|
docker compose -f docker-compose.prod.yml up -d
|
|
|
|
# 查看日志
|
|
docker compose -f docker-compose.prod.yml logs -f authorization-service
|
|
|
|
# 扩容
|
|
docker compose -f docker-compose.prod.yml up -d --scale authorization-service=3
|
|
```
|
|
|
|
---
|
|
|
|
## Kubernetes 部署
|
|
|
|
### Deployment
|
|
|
|
```yaml
|
|
# k8s/deployment.yaml
|
|
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: authorization-service
|
|
namespace: rwadurian
|
|
labels:
|
|
app: authorization-service
|
|
spec:
|
|
replicas: 3
|
|
selector:
|
|
matchLabels:
|
|
app: authorization-service
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app: authorization-service
|
|
spec:
|
|
containers:
|
|
- name: authorization-service
|
|
image: your-registry/authorization-service:latest
|
|
ports:
|
|
- containerPort: 3002
|
|
env:
|
|
- name: NODE_ENV
|
|
value: "production"
|
|
- name: PORT
|
|
value: "3002"
|
|
- name: DATABASE_URL
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: authorization-secrets
|
|
key: database-url
|
|
- name: REDIS_HOST
|
|
valueFrom:
|
|
configMapKeyRef:
|
|
name: authorization-config
|
|
key: redis-host
|
|
- name: REDIS_PORT
|
|
valueFrom:
|
|
configMapKeyRef:
|
|
name: authorization-config
|
|
key: redis-port
|
|
- name: KAFKA_BROKERS
|
|
valueFrom:
|
|
configMapKeyRef:
|
|
name: authorization-config
|
|
key: kafka-brokers
|
|
- name: JWT_SECRET
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: authorization-secrets
|
|
key: jwt-secret
|
|
resources:
|
|
requests:
|
|
memory: "512Mi"
|
|
cpu: "250m"
|
|
limits:
|
|
memory: "1Gi"
|
|
cpu: "1000m"
|
|
readinessProbe:
|
|
httpGet:
|
|
path: /health
|
|
port: 3002
|
|
initialDelaySeconds: 10
|
|
periodSeconds: 5
|
|
livenessProbe:
|
|
httpGet:
|
|
path: /health
|
|
port: 3002
|
|
initialDelaySeconds: 30
|
|
periodSeconds: 10
|
|
imagePullSecrets:
|
|
- name: registry-credentials
|
|
```
|
|
|
|
### Service
|
|
|
|
```yaml
|
|
# k8s/service.yaml
|
|
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: authorization-service
|
|
namespace: rwadurian
|
|
spec:
|
|
selector:
|
|
app: authorization-service
|
|
ports:
|
|
- port: 3002
|
|
targetPort: 3002
|
|
type: ClusterIP
|
|
```
|
|
|
|
### ConfigMap
|
|
|
|
```yaml
|
|
# k8s/configmap.yaml
|
|
|
|
apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata:
|
|
name: authorization-config
|
|
namespace: rwadurian
|
|
data:
|
|
redis-host: "redis-master.redis.svc.cluster.local"
|
|
redis-port: "6379"
|
|
kafka-brokers: "kafka-0.kafka.svc.cluster.local:9092,kafka-1.kafka.svc.cluster.local:9092"
|
|
```
|
|
|
|
### Secret
|
|
|
|
```yaml
|
|
# k8s/secret.yaml
|
|
|
|
apiVersion: v1
|
|
kind: Secret
|
|
metadata:
|
|
name: authorization-secrets
|
|
namespace: rwadurian
|
|
type: Opaque
|
|
stringData:
|
|
database-url: "postgresql://user:password@postgres:5432/authorization"
|
|
jwt-secret: "your-production-jwt-secret"
|
|
```
|
|
|
|
### Ingress
|
|
|
|
```yaml
|
|
# k8s/ingress.yaml
|
|
|
|
apiVersion: networking.k8s.io/v1
|
|
kind: Ingress
|
|
metadata:
|
|
name: authorization-ingress
|
|
namespace: rwadurian
|
|
annotations:
|
|
nginx.ingress.kubernetes.io/rewrite-target: /
|
|
spec:
|
|
ingressClassName: nginx
|
|
rules:
|
|
- host: api.rwadurian.com
|
|
http:
|
|
paths:
|
|
- path: /authorization
|
|
pathType: Prefix
|
|
backend:
|
|
service:
|
|
name: authorization-service
|
|
port:
|
|
number: 3002
|
|
```
|
|
|
|
### HPA (自动扩缩容)
|
|
|
|
```yaml
|
|
# k8s/hpa.yaml
|
|
|
|
apiVersion: autoscaling/v2
|
|
kind: HorizontalPodAutoscaler
|
|
metadata:
|
|
name: authorization-service-hpa
|
|
namespace: rwadurian
|
|
spec:
|
|
scaleTargetRef:
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
name: authorization-service
|
|
minReplicas: 2
|
|
maxReplicas: 10
|
|
metrics:
|
|
- type: Resource
|
|
resource:
|
|
name: cpu
|
|
target:
|
|
type: Utilization
|
|
averageUtilization: 70
|
|
- type: Resource
|
|
resource:
|
|
name: memory
|
|
target:
|
|
type: Utilization
|
|
averageUtilization: 80
|
|
```
|
|
|
|
### 部署命令
|
|
|
|
```bash
|
|
# 创建命名空间
|
|
kubectl create namespace rwadurian
|
|
|
|
# 应用配置
|
|
kubectl apply -f k8s/
|
|
|
|
# 查看部署状态
|
|
kubectl get pods -n rwadurian -l app=authorization-service
|
|
|
|
# 查看日志
|
|
kubectl logs -f -n rwadurian -l app=authorization-service
|
|
|
|
# 扩缩容
|
|
kubectl scale deployment authorization-service -n rwadurian --replicas=5
|
|
```
|
|
|
|
---
|
|
|
|
## 数据库迁移
|
|
|
|
### 迁移策略
|
|
|
|
1. **新部署**: 自动运行所有迁移
|
|
2. **升级部署**: 先迁移数据库,再部署新版本
|
|
3. **回滚**: 支持向下迁移
|
|
|
|
### 迁移命令
|
|
|
|
```bash
|
|
# 创建新迁移
|
|
npx prisma migrate dev --name add_new_field
|
|
|
|
# 应用迁移(生产)
|
|
npx prisma migrate deploy
|
|
|
|
# 重置数据库(仅开发)
|
|
npx prisma migrate reset
|
|
|
|
# 查看迁移状态
|
|
npx prisma migrate status
|
|
```
|
|
|
|
### 迁移脚本
|
|
|
|
```bash
|
|
#!/bin/bash
|
|
# scripts/migrate.sh
|
|
|
|
set -e
|
|
|
|
echo "Running database migrations..."
|
|
|
|
# 等待数据库就绪
|
|
until npx prisma migrate status > /dev/null 2>&1; do
|
|
echo "Waiting for database..."
|
|
sleep 2
|
|
done
|
|
|
|
# 运行迁移
|
|
npx prisma migrate deploy
|
|
|
|
echo "Migrations completed successfully!"
|
|
```
|
|
|
|
### Kubernetes Job (迁移)
|
|
|
|
```yaml
|
|
# k8s/migration-job.yaml
|
|
|
|
apiVersion: batch/v1
|
|
kind: Job
|
|
metadata:
|
|
name: authorization-migration
|
|
namespace: rwadurian
|
|
spec:
|
|
template:
|
|
spec:
|
|
containers:
|
|
- name: migration
|
|
image: your-registry/authorization-service:latest
|
|
command: ["npx", "prisma", "migrate", "deploy"]
|
|
env:
|
|
- name: DATABASE_URL
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: authorization-secrets
|
|
key: database-url
|
|
restartPolicy: Never
|
|
backoffLimit: 3
|
|
```
|
|
|
|
---
|
|
|
|
## 监控与日志
|
|
|
|
### 健康检查端点
|
|
|
|
```typescript
|
|
// src/health/health.controller.ts
|
|
|
|
@Controller('health')
|
|
export class HealthController {
|
|
constructor(
|
|
private health: HealthCheckService,
|
|
private db: PrismaHealthIndicator,
|
|
private redis: RedisHealthIndicator,
|
|
) {}
|
|
|
|
@Get()
|
|
@HealthCheck()
|
|
check() {
|
|
return this.health.check([
|
|
() => this.db.pingCheck('database'),
|
|
() => this.redis.pingCheck('redis'),
|
|
])
|
|
}
|
|
|
|
@Get('live')
|
|
live() {
|
|
return { status: 'ok' }
|
|
}
|
|
|
|
@Get('ready')
|
|
@HealthCheck()
|
|
ready() {
|
|
return this.health.check([
|
|
() => this.db.pingCheck('database'),
|
|
])
|
|
}
|
|
}
|
|
```
|
|
|
|
### Prometheus 指标
|
|
|
|
```typescript
|
|
// src/metrics/metrics.module.ts
|
|
|
|
import { PrometheusModule } from '@willsoto/nestjs-prometheus'
|
|
|
|
@Module({
|
|
imports: [
|
|
PrometheusModule.register({
|
|
defaultMetrics: {
|
|
enabled: true,
|
|
},
|
|
path: '/metrics',
|
|
}),
|
|
],
|
|
})
|
|
export class MetricsModule {}
|
|
```
|
|
|
|
### 日志配置
|
|
|
|
```typescript
|
|
// src/main.ts
|
|
|
|
import { WinstonModule } from 'nest-winston'
|
|
import * as winston from 'winston'
|
|
|
|
const app = await NestFactory.create(AppModule, {
|
|
logger: WinstonModule.createLogger({
|
|
transports: [
|
|
new winston.transports.Console({
|
|
format: winston.format.combine(
|
|
winston.format.timestamp(),
|
|
winston.format.json(),
|
|
),
|
|
}),
|
|
],
|
|
}),
|
|
})
|
|
```
|
|
|
|
### 结构化日志格式
|
|
|
|
```json
|
|
{
|
|
"timestamp": "2024-01-20T10:30:00.000Z",
|
|
"level": "info",
|
|
"message": "Authorization created",
|
|
"service": "authorization-service",
|
|
"traceId": "abc-123",
|
|
"userId": "user-001",
|
|
"authorizationId": "auth-456",
|
|
"roleType": "AUTH_PROVINCE_COMPANY"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 故障排除
|
|
|
|
### 常见问题
|
|
|
|
#### 1. 数据库连接失败
|
|
|
|
```bash
|
|
# 检查数据库连接
|
|
npx prisma db pull
|
|
|
|
# 查看连接字符串
|
|
echo $DATABASE_URL
|
|
|
|
# 测试网络连通性
|
|
nc -zv db-host 5432
|
|
```
|
|
|
|
#### 2. Redis 连接失败
|
|
|
|
```bash
|
|
# 测试 Redis 连接
|
|
redis-cli -h redis-host -p 6379 -a password ping
|
|
|
|
# 查看 Redis 状态
|
|
redis-cli -h redis-host info
|
|
```
|
|
|
|
#### 3. Kafka 连接失败
|
|
|
|
```bash
|
|
# 列出 topics
|
|
kafka-topics.sh --bootstrap-server kafka:9092 --list
|
|
|
|
# 查看 consumer groups
|
|
kafka-consumer-groups.sh --bootstrap-server kafka:9092 --list
|
|
```
|
|
|
|
#### 4. Pod 启动失败
|
|
|
|
```bash
|
|
# 查看 Pod 状态
|
|
kubectl describe pod <pod-name> -n rwadurian
|
|
|
|
# 查看容器日志
|
|
kubectl logs <pod-name> -n rwadurian
|
|
|
|
# 进入容器调试
|
|
kubectl exec -it <pod-name> -n rwadurian -- sh
|
|
```
|
|
|
|
### 性能调优
|
|
|
|
#### 数据库连接池
|
|
|
|
```typescript
|
|
// prisma/schema.prisma
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
// 连接池配置
|
|
// ?connection_limit=20&pool_timeout=30
|
|
}
|
|
```
|
|
|
|
#### Redis 连接池
|
|
|
|
```typescript
|
|
// src/infrastructure/cache/redis.service.ts
|
|
|
|
const redis = new Redis({
|
|
host: process.env.REDIS_HOST,
|
|
port: parseInt(process.env.REDIS_PORT),
|
|
password: process.env.REDIS_PASSWORD,
|
|
maxRetriesPerRequest: 3,
|
|
enableReadyCheck: true,
|
|
// 连接池大小
|
|
lazyConnect: true,
|
|
})
|
|
```
|
|
|
|
### 运维命令
|
|
|
|
```bash
|
|
# 查看服务状态
|
|
kubectl get all -n rwadurian
|
|
|
|
# 查看资源使用
|
|
kubectl top pods -n rwadurian
|
|
|
|
# 滚动更新
|
|
kubectl rollout restart deployment/authorization-service -n rwadurian
|
|
|
|
# 回滚
|
|
kubectl rollout undo deployment/authorization-service -n rwadurian
|
|
|
|
# 查看回滚历史
|
|
kubectl rollout history deployment/authorization-service -n rwadurian
|
|
```
|
|
|
|
---
|
|
|
|
## 部署检查清单
|
|
|
|
### 部署前
|
|
|
|
- [ ] 环境变量配置完成
|
|
- [ ] 数据库迁移已准备
|
|
- [ ] 镜像已构建并推送
|
|
- [ ] 配置文件已验证
|
|
- [ ] 密钥已配置
|
|
|
|
### 部署中
|
|
|
|
- [ ] 数据库迁移成功
|
|
- [ ] Pod 启动正常
|
|
- [ ] 健康检查通过
|
|
- [ ] 服务可访问
|
|
|
|
### 部署后
|
|
|
|
- [ ] 功能测试通过
|
|
- [ ] 监控指标正常
|
|
- [ ] 日志无异常
|
|
- [ ] 通知相关人员
|
|
|
|
---
|
|
|
|
## 参考资源
|
|
|
|
- [Docker 官方文档](https://docs.docker.com/)
|
|
- [Kubernetes 官方文档](https://kubernetes.io/docs/)
|
|
- [Prisma 部署指南](https://www.prisma.io/docs/guides/deployment)
|
|
- [NestJS 部署指南](https://docs.nestjs.com/faq/common-errors)
|