Docker 容器化与 CI/CD
一、多阶段 Dockerfile
多阶段构建:编译产物小、不含开发依赖、安全性高:
dockerfile
# ── 阶段 1:依赖安装 ─────────────────────────────
FROM node:20-alpine AS deps
WORKDIR /app
# 单独复制 package*.json,利用 Docker 层缓存
# 只有依赖变化时才重新 npm ci,代码变化不触发重装
COPY package*.json ./
RUN npm ci --frozen-lockfile
# ── 阶段 2:TypeScript 编译 ───────────────────────
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# 如果有 Prisma,需要先生成客户端
# RUN npx prisma generate
RUN npm run build
# 此时 dist/ 目录包含编译后的 JS
# ── 阶段 3:生产镜像(最小化)───────────────────────
FROM node:20-alpine AS production
WORKDIR /app
# 只添加必要的系统包(如 curl 用于健康检查)
RUN apk add --no-cache curl
# 以非 root 用户运行(安全最佳实践)
RUN addgroup -g 1001 -S nodejs && adduser -S nestjs -u 1001
USER nestjs
COPY package*.json ./
# 只安装生产依赖
RUN npm ci --only=production --frozen-lockfile && npm cache clean --force
COPY --from=builder --chown=nestjs:nodejs /app/dist ./dist
# 如果有 Prisma 迁移文件
# COPY --from=builder --chown=nestjs:nodejs /app/prisma ./prisma
EXPOSE 3000
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
CMD ["node", "dist/main"].dockerignore(避免不必要的文件进入构建上下文):
node_modules
dist
.git
.env
.env.*
coverage
logs
*.md
*.spec.ts二、Docker Compose(本地开发)
yaml
# docker-compose.yml
version: '3.8'
services:
# ── 应用 ──────────────────────────────────────
api:
build:
context: .
target: builder # 开发时用 builder 阶段(含 devDeps)
volumes:
- .:/app # 挂载源码(热重载)
- /app/node_modules
command: npm run start:dev
ports:
- '3000:3000'
environment:
NODE_ENV: development
DATABASE_URL: postgres://postgres:postgres@postgres:5432/myapp_dev
REDIS_URL: redis://redis:6379
JWT_SECRET: dev-secret-change-in-production
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
# ── PostgreSQL ─────────────────────────────────
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: myapp_dev
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- '5432:5432'
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 5s
timeout: 5s
retries: 5
# ── Redis ──────────────────────────────────────
redis:
image: redis:7-alpine
ports:
- '6379:6379'
command: redis-server --appendonly yes
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:bash
# 常用命令
docker-compose up -d # 后台启动所有服务
docker-compose logs -f api # 查看 API 日志
docker-compose exec api sh # 进入容器 Shell
docker-compose down -v # 停止并删除数据卷(清空数据库)三、GitHub Actions CI 流水线
yaml
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
# ── 单元测试 ────────────────────────────────────
test:
name: Unit Tests & Coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests with coverage
run: npm run test:cov
env:
NODE_ENV: test
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage/lcov.info
# ── E2E 测试 ────────────────────────────────────
e2e:
name: E2E Tests
runs-on: ubuntu-latest
needs: test # 单元测试通过后才跑 E2E
services:
postgres:
image: postgres:15
env:
POSTGRES_DB: nestjs_test
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports: ['5432:5432']
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports: ['6379:6379']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Run E2E tests
run: npm run test:e2e
env:
NODE_ENV: test
DATABASE_URL: postgres://test:test@localhost:5432/nestjs_test
REDIS_URL: redis://localhost:6379
JWT_SECRET: test-secret-for-ci-needs-32-chars!!
# ── 构建 Docker 镜像 ────────────────────────────
build:
name: Build & Push Docker Image
runs-on: ubuntu-latest
needs: [test, e2e]
if: github.ref == 'refs/heads/main' # 只在主分支
steps:
- uses: actions/checkout@v4
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
target: production
push: true
tags: |
myorg/myapp:latest
myorg/myapp:${{ github.sha }}
cache-from: type=gha # 使用 GitHub Actions 缓存加速构建
cache-to: type=gha,mode=max四、CD 部署流水线
yaml
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
tags: ['v*.*.*'] # 打版本 tag 时触发
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.PROD_HOST }}
username: ${{ secrets.PROD_USER }}
key: ${{ secrets.PROD_SSH_KEY }}
script: |
cd /opt/myapp
docker-compose pull
docker-compose up -d --no-deps api
# 等待健康检查通过
docker-compose exec -T api curl -f http://localhost:3000/health
echo "部署成功 ✓"五、环境变量管理
yaml
# 生产环境:通过 GitHub Secrets 注入,不使用 .env 文件
# 在仓库设置 → Secrets and variables → Actions 中配置
# 敏感配置:DATABASE_URL, JWT_SECRET 等 → GitHub Secrets
# 非敏感配置:NODE_ENV, PORT 等 → GitHub Variablesdockerfile
# Dockerfile 中不硬编码任何配置
# 所有配置通过 ENV 或运行时 -e 注入六、容器安全实践
dockerfile
# ✅ 使用特定版本,不用 latest
FROM node:20.11.0-alpine3.19 AS production
# ✅ 非 root 用户运行
USER nestjs
# ✅ 最小化镜像(alpine 比 debian 小 70%)
FROM node:20-alpine
# ✅ 定期扫描镜像漏洞
# 在 CI 中添加
- name: Scan image
uses: aquasecurity/trivy-action@master
with:
image-ref: myorg/myapp:${{ github.sha }}
exit-code: '1' # 发现高危漏洞时让 CI 失败
severity: 'CRITICAL,HIGH'七、完整 package.json scripts
json
{
"scripts": {
"build": "nest build",
"start": "node dist/main",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:e2e": "jest --config ./test/jest-e2e.json",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"migration:generate": "typeorm migration:generate -d src/data-source.ts",
"migration:run": "typeorm migration:run -d src/data-source.ts",
"migration:revert": "typeorm migration:revert -d src/data-source.ts",
"docker:dev": "docker-compose up -d",
"docker:down": "docker-compose down",
"docker:build": "docker build -t myapp:local --target production ."
}
}可运行 Demo:
practice/07-extensions— docker-compose.yml(Redis 服务)参考
常见错误
| 错误 | 原因 | 解决 |
|---|---|---|
| 镜像体积过大 | 包含了 devDependencies 或源码 | 多阶段构建:builder 阶段编译,production 阶段只复制 dist/ 和 node_modules(生产依赖) |
| 容器启动慢 | 未利用 Docker 层缓存 | COPY package.json → npm ci → COPY src/ 顺序,package.json 不变时复用缓存 |
| CI 测试通过但生产失败 | 测试用 Mock,生产用真实服务 | 在 CI 中运行 e2e 测试(带真实 DB/Redis 服务),而不只是单元测试 |
| 健康检查失败导致容器重启循环 | 应用启动慢但健康检查超时短 | 调整 startPeriod(给应用足够启动时间)和 retries 参数 |