Skip to content

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 Variables
dockerfile
# 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.jsonnpm ciCOPY src/ 顺序,package.json 不变时复用缓存
CI 测试通过但生产失败测试用 Mock,生产用真实服务在 CI 中运行 e2e 测试(带真实 DB/Redis 服务),而不只是单元测试
健康检查失败导致容器重启循环应用启动慢但健康检查超时短调整 startPeriod(给应用足够启动时间)和 retries 参数

NestJS 深度学习体系