{T}

NestJS容器化部署实战

NestJS容器化部署实战

学习目标:掌握 NestJS 应用的 Docker 容器化部署、Prisma 集成、环境变量配置、日志映射、Docker Compose 多服务管理。


一、NestJS 与 Nuxt3 容器化差异

1.1 核心差异对比

code
NestJS vs Nuxt3 容器化对比:
│
├── 构建产物
│   ├── Nuxt3:.output 目录(自包含 node_modules)
│   └── NestJS:dist 目录(需要外部 node_modules)
│
├── 依赖管理
│   ├── Nuxt3:bundleDependencies 自动打包依赖
│   └── NestJS:需要手动安装生产依赖
│
├── Prisma 集成 
│   ├── Nuxt3:通常无数据库操作
│   └── NestJS:需要 prisma generate 生成 client
│
├── 启动方式
│   ├── Nuxt3:node .output/server/index.mjs
│   └── NestJS:node dist/main.js
│
└── 环境变量
    ├── Nuxt3:BASE_URL 等 API 地址
    └── NestJS:DATABASE_URL、JWT_SECRET 等

1.2 NestJS 容器化特殊需求

code
NestJS 容器化关键点:
│
├── 1. Prisma Client 生成 
│   ├── 执行 prisma generate
│   ├── 生成 node_modules/.prisma/client
│   └── 数据库操作必需文件
│
├── 2. 生产依赖安装 
│   ├── npm install --production
│   ├── 或 npm install --omit=dev
│   └── 不包含开发依赖
│
├── 3. 文件拷贝顺序 
│   ├── 先拷贝 package.json
│   ├── 安装生产依赖
│   ├── 拷贝 prisma client
│   └── 最后拷贝 dist 目录
│
└── 4. 数据库连接 
    ├── DATABASE_URL 环境变量
    ├── 数据库服务运行
    └── 网络连通性

二、Dockerfile 配置详解

2.1 package.json 脚本调整

问题分析

json
//  原始 package.json
{
  "scripts": {
    "preinstall": "prisma generate"
  }
}

问题

  • preinstall 会在 npm install 前自动执行
  • 构建阶段可能没有 prisma client 类型文件
  • 导致安装失败或找不到类型

解决方案

json
//  调整后的 package.json
{
  "scripts": {
    "generate": "prisma generate"
  }
}

说明

  • preinstall 改为手动脚本 generate
  • 在合适的构建阶段手动执行
  • 避免自动执行导致的问题

2.2 完整 Dockerfile 配置

dockerfile
# ==================== 构建阶段 ====================
FROM node:18-alpine AS builder

# 使用 node 用户(安全配置)
USER node
WORKDIR /home/node/app

# 设置淘宝源(国内加速)
RUN npm config set registry https://registry.npmmirror.com

# 复制依赖配置文件
COPY --chown=node:node package*.json ./
COPY --chown=node:node prisma ./prisma/

# 安装所有依赖(包括开发依赖)
RUN npm install

# 生成 Prisma Client
RUN npm run generate
# 或直接执行:npx prisma generate

# 复制源码
COPY --chown=node:node . .

# 构建应用
RUN npm run build

# ==================== 生产阶段 ====================
FROM node:18-alpine AS production

USER node
WORKDIR /home/node/app

# 复制 package.json
COPY --from=builder --chown=node:node /home/node/app/package*.json ./

# 安装生产依赖
RUN npm install --production
# 或使用:npm install --omit=dev

# 拷贝 Prisma Client(关键步骤)
COPY --from=builder --chown=node:node /home/node/app/node_modules/.prisma ./node_modules/.prisma
COPY --from=builder --chown=node:node /home/node/app/node_modules/@prisma ./node_modules/@prisma

# 拷贝构建产物
COPY --from=builder --chown=node:node /home/node/app/dist ./dist

# 拷贝 Prisma schema(运行迁移时需要)
COPY --from=builder --chown=node:node /home/node/app/prisma ./prisma

# 暴露端口
EXPOSE 3000

# 设置环境变量
ENV NODE_ENV=production

# 启动应用
CMD ["node", "dist/main.js"]

2.3 关键配置详解

1. Prisma Client 拷贝

为什么需要手动拷贝?

code
Prisma Client 文件结构:
│
├── node_modules/.prisma/client/
│   ├── index.js           # 入口文件
│   ├── schema.prisma      # Schema 副本
│   ├── runtime/           # 运行时库
│   └── default.js         # 默认导出
│
└── node_modules/@prisma/client/
    ├── index.js
    └── ...(符号链接)

原因

  • npm install --production 不会安装开发依赖
  • Prisma CLI 是开发依赖,不会安装
  • 但运行时需要 Prisma Client
  • 必须从构建阶段拷贝

2. 生产依赖安装

bash
# 两种方式等价
npm install --production
npm install --omit=dev

# 区别
# --production:npm 6+ 支持
# --omit=dev:npm 8.10+ 支持,更语义化

3. 文件拷贝顺序的重要性

code
文件拷贝顺序(关键):
│
├── 1. package.json
│   └── 定义依赖版本
│
├── 2. npm install --production
│   └── 安装生产依赖到 node_modules
│
├── 3. 拷贝 Prisma Client 
│   └── 覆盖/添加到 node_modules
│
└── 4. 拷贝 dist 目录
    └── 最后拷贝构建产物

为什么这个顺序很重要?
├── 如果先拷贝 dist,再安装依赖
│   └── 可能覆盖某些文件(极少见)
│
└── 正确顺序确保依赖完整
    └── Prisma Client 正确集成

2.4 .dockerignore 配置

plaintext
# .dockerignore
node_modules
dist
.git
.github
.env*
*.log
.DS_Store
logs

三、环境变量配置

3.1 数据库连接配置

方式一:命令行传递

bash
# 停止并删除旧容器
docker stop test
docker rm test

# 使用环境变量运行
docker run \
  -d \
  --name nest-app \
  -p 8000:3000 \
  -e DATABASE_URL="postgresql://user:password@host:5432/db" \
  nest-app:1.0

方式二:Docker Compose + .env 文件(推荐)

.env 文件

bash
# .env
DATABASE_URL="postgresql://postgres:password@postgres:5432/mydb"
JWT_SECRET="your-secret-key"
PORT=3000
NODE_ENV=production

docker-compose.yml

yaml
version: '3.8'

services:
  # 数据库服务
  postgres:
    image: postgres:15-alpine
    container_name: postgres-db
    restart: always
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB: mydb
    ports:
      - "5432:5432"
    volumes:
      - postgres-data:/var/lib/postgresql/data
    networks:
      - app-network

  # NestJS 应用
  nest-app:
    image: nest-app:1.0
    container_name: nest-app-prod
    restart: always
    ports:
      - "8000:3000"
    env_file:
      - .env  # 引用环境变量文件
    depends_on:
      - postgres
    networks:
      - app-network

volumes:
  postgres-data:

networks:
  app-network:
    driver: bridge

3.2 环境变量对比

方式适用场景优点缺点
-e 参数测试、临时部署简单快速参数多时冗长
environment简单项目配置集中敏感信息暴露
env_file生产环境安全、可维护需要额外文件

四、日志映射与持久化

4.1 为什么需要日志映射?

code
日志映射的必要性:
│
├── 1. 问题排查 
│   ├── 容器删除后日志丢失
│   ├── 需要保留历史日志
│   └── 便于回溯问题
│
├── 2. 日志分析 
│   ├── 统一日志收集
│   ├── ELK 集成
│   └── 监控告警
│
├── 3. 合规要求 
│   ├── 审计日志保留
│   ├── 安全合规
│   └── 数据备份
│
└── 4. 便捷查看 
    ├── 无需进入容器
    ├── 宿主机直接查看
    └── 使用熟悉的工具

4.2 Volume 映射配置

Docker Compose 配置

yaml
version: '3.8'

services:
  nest-app:
    image: nest-app:1.0
    container_name: nest-app-prod
    restart: always
    ports:
      - "8000:3000"
    env_file:
      - .env
    volumes:
      # 日志映射:容器路径 → 宿主机路径
      - ./logs:/home/node/app/logs
      # 其他可能的映射
      - ./uploads:/home/node/app/uploads
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

NestJS 日志配置示例

typescript
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as fs from 'fs';
import * as path from 'path';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    logger: ['error', 'warn', 'log'],
  });

  // 确保日志目录存在
  const logDir = path.join(__dirname, '..', 'logs');
  if (!fs.existsSync(logDir)) {
    fs.mkdirSync(logDir, { recursive: true });
  }

  await app.listen(3000);
}
bootstrap();

4.3 Windows 注意事项

问题:Windows 上相对路径映射可能失败

解决方案

yaml
#  不推荐(Windows 可能失败)
volumes:
  - ./logs:/home/node/app/logs

#  推荐(使用绝对路径)
volumes:
  - /c/Users/YourName/project/logs:/home/node/app/logs
  # 或 Windows 路径格式
  - C:\Users\YourName\project\logs:/home/node/app/logs

验证映射

bash
# 进入容器查看
docker exec -it nest-app-prod sh
ls /home/node/app/logs

# 在宿主机查看
ls ./logs

# 两者应该内容一致

五、完整部署流程

5.1 部署步骤

bash
# ==================== 第一步:构建镜像 ====================
docker build -t nest-app:1.0 .

# 查看构建结果
docker images | grep nest-app

# ==================== 第二步:运行容器(测试) ====================
docker run -d --name test -p 8000:3000 nest-app:1.0

# 查看运行状态
docker ps

# 查看日志
docker logs -f test

# ==================== 第三步:测试接口 ====================
# 访问健康检查接口
curl http://localhost:8000/api

# 预期错误(未配置数据库)
# PrismaClientInitializationError: Can't reach database server

# ==================== 第四步:配置环境变量 ====================
# 停止并删除测试容器
docker stop test
docker rm test

# 创建 .env 文件
cat > .env << EOF
DATABASE_URL="postgresql://postgres:password@postgres:5432/mydb"
JWT_SECRET="your-secret-key"
EOF

# ==================== 第五步:使用 Docker Compose 启动 ====================
docker compose up -d

# 查看运行状态
docker compose ps

# 查看日志
docker compose logs -f nest-app

# ==================== 第六步:验证服务 ====================
curl http://localhost:8000/api

# 预期输出:正常响应数据

5.2 常用运维命令

bash
# ==================== 服务管理 ====================
# 启动服务
docker compose up -d

# 停止服务
docker compose down

# 重启服务
docker compose restart

# 重新构建并启动
docker compose up -d --build

# ==================== 日志查看 ====================
# 查看所有服务日志
docker compose logs

# 查看特定服务日志
docker compose logs nest-app

# 实时查看日志
docker compose logs -f nest-app

# 查看最后 100 行
docker compose logs --tail 100 nest-app

# ==================== 进入容器 ====================
# 进入应用容器
docker compose exec nest-app sh

# 进入数据库容器
docker compose exec postgres sh

# 以 root 用户进入
docker compose exec -u root nest-app sh

# ==================== 数据库操作 ====================
# 运行 Prisma 迁移
docker compose exec nest-app npx prisma migrate deploy

# 打开 Prisma Studio
docker compose exec nest-app npx prisma studio

# 查看数据库
docker compose exec postgres psql -U postgres -d mydb

六、常见问题与解决方案

6.1 问题排查流程

code
NestJS 容器问题排查:
│
├── 1. 构建失败
│   ├── 检查 Dockerfile 语法
│   ├── 查看构建日志定位错误步骤
│   ├── 验证 .dockerignore 配置
│   └── 检查 prisma generate 是否执行
│
├── 2. 启动失败
│   ├── docker logs 查看容器日志
│   ├── 检查环境变量是否正确
│   ├── 验证数据库连接
│   └── 检查端口映射
│
├── 3. 数据库连接失败 
│   ├── DATABASE_URL 配置错误
│   ├── 数据库服务未启动
│   ├── 网络不通(容器间通信)
│   └── 防火墙阻止
│
└── 4. Prisma 错误
    ├── Prisma Client 未生成
    ├── 未拷贝 .prisma 目录
    └── Schema 文件缺失

6.2 常见错误汇总

错误信息原因分析解决方案
PrismaClientInitializationError: Can't reach database serverDATABASE_URL 未配置或错误配置正确的环境变量
Cannot find module '@prisma/client'Prisma Client 未拷贝在 Dockerfile 中添加 COPY 指令
Error: P1001: Can't reach database server数据库服务未运行启动数据库容器
Permission denied文件权限问题使用 --chown=node:node
Port 3000 is already in use端口冲突更换映射端口
ENOENT: no such file or directory文件路径错误检查 COPY 和 WORKDIR

6.3 数据库连接问题详解

问题:DATABASE_URL 配置

bash
#  错误示例(使用 localhost)
DATABASE_URL="postgresql://postgres:password@localhost:5432/mydb"

# 问题:
# - localhost 在容器内指向容器自身
# - 数据库在另一个容器中
# - 网络不通
bash
#  正确示例(使用服务名)
DATABASE_URL="postgresql://postgres:password@postgres:5432/mydb"

# 说明:
# - postgres 是 docker-compose.yml 中定义的服务名
# - Docker Compose 自动创建 DNS 解析
# - 容器间通过服务名通信

网络配置验证

bash
# 进入应用容器
docker compose exec nest-app sh

# 测试数据库连通性
ping postgres

# 使用 nc 测试端口
nc -zv postgres 5432

# 预期输出:Connection to postgres 5432 port [tcp/postgresql] succeeded!

七、Docker Compose 多服务编排

7.1 完整生产级配置

yaml
version: '3.8'

services:
  # PostgreSQL 数据库
  postgres:
    image: postgres:15-alpine
    container_name: postgres-prod
    restart: always
    environment:
      POSTGRES_USER: ${DB_USER:-postgres}
      POSTGRES_PASSWORD: ${DB_PASSWORD:-password}
      POSTGRES_DB: ${DB_NAME:-mydb}
    ports:
      - "5432:5432"
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - app-network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

  # NestJS 应用
  nest-app:
    image: nest-app:1.0
    container_name: nest-app-prod
    restart: always
    ports:
      - "8000:3000"
    env_file:
      - .env
    volumes:
      - ./logs:/home/node/app/logs
      - ./uploads:/home/node/app/uploads
    depends_on:
      postgres:
        condition: service_healthy
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  # Nginx 反向代理(可选)
  nginx:
    image: nginx:alpine
    container_name: nginx-proxy
    restart: always
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - nest-app
    networks:
      - app-network

volumes:
  postgres-data:
    driver: local

networks:
  app-network:
    driver: bridge

7.2 depends_on 配置说明

yaml
# 方式一:简单依赖(不等待启动完成)
depends_on:
  - postgres

# 方式二:条件依赖(等待健康检查通过)
depends_on:
  postgres:
    condition: service_healthy

# 说明:
# - service_healthy:等待健康检查通过
# - service_started:等待容器启动(默认)
# - service_completed_successfully:等待容器执行完成

7.3 健康检查配置

yaml
# PostgreSQL 健康检查
healthcheck:
  test: ["CMD-SHELL", "pg_isready -U postgres"]
  interval: 10s      # 检查间隔
  timeout: 5s        # 超时时间
  retries: 5         # 重试次数
  start_period: 10s  # 启动等待时间

# NestJS 应用健康检查
healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 40s  # 给应用足够的启动时间

八、生产环境最佳实践

8.1 安全配置清单

code
生产环境安全配置:
│
├── 1. 非 root 用户运行 
│   ├── USER node
│   ├── WORKDIR /home/node/app
│   └── COPY --chown=node:node
│
├── 2. 环境变量安全 
│   ├── .env 文件不入 Git
│   ├── 使用 Docker Secrets(Kubernetes)
│   └── 敏感信息加密存储
│
├── 3. 网络隔离 
│   ├── 用户定义网络
│   ├── 不暴露不必要的端口
│   └── 数据库端口仅内网访问
│
├── 4. 镜像安全 
│   ├── 使用官方基础镜像
│   ├── 定期更新基础镜像
│   ├── 镜像漏洞扫描
│   └── 使用特定版本标签(不用 latest)
│
└── 5. 日志管理 
    ├── 日志映射到宿主机
    ├── 日志轮转配置
    └── 统一日志收集系统

8.2 性能优化

dockerfile
# ==================== 构建缓存优化 ====================
# 先复制依赖文件,利用 Docker 缓存
COPY package*.json ./
COPY prisma ./prisma/
RUN npm install

# 再复制源码(源码变化频繁)
COPY . .
RUN npm run build

# ==================== 层合并优化 ====================
# 合并多个 RUN 命令
RUN npm install && \
    npm run build && \
    npm cache clean --force

# ==================== 基础镜像选择 ====================
# 生产环境推荐
FROM node:18-alpine  # 体积小(~170MB)

# 开发环境可用
FROM node:18-slim    # 兼容性好(~240MB)
FROM node:18         # 功能全(~900MB)

8.3 监控与告警

yaml
# docker-compose.yml 添加监控
services:
  # 应用监控(可选)
  prometheus:
    image: prom/prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"

  grafana:
    image: grafana/grafana
    ports:
      - "3001:3000"
    volumes:
      - grafana-data:/var/lib/grafana

volumes:
  grafana-data:

九、学习要点总结

核心要点

  1. NestJS 与 Nuxt3 容器化的核心差异

    • NestJS 需要手动安装生产依赖
    • 必须拷贝 Prisma Client 文件
    • 需要手动执行 prisma generate
    • 不像 Nuxt3 有 bundleDependencies
  2. Prisma Client 是容器化的关键点

    • 调整 package.jsonpreinstall 脚本
    • 构建阶段生成 client
    • 生产阶段从 builder 拷贝
    • 确保 .prisma@prisma 目录都拷贝
  3. 数据库连接配置要点

    • DATABASE_URL 使用服务名(不是 localhost)
    • Docker Compose 自动 DNS 解析
    • depends_on 等待数据库就绪
    • 健康检查确保服务可用
  4. 日志映射提高运维效率

    • Volume 映射日志目录
    • 宿主机直接查看日志
    • 便于问题排查和审计
    • Windows 使用绝对路径
  5. Docker Compose 简化多服务管理

    • 一键启动完整环境
    • 环境变量统一管理
    • 服务依赖自动处理
    • 网络自动配置

十、延伸学习资源

官方文档

进阶主题

code
推荐学习路径:
│
├── 1. CI/CD 自动化
│   ├── GitHub Actions
│   ├── 自动化测试
│   └── 自动化部署
│
├── 2. Kubernetes 部署
│   ├── Deployment 配置
│   ├── Service 配置
│   ├── Ingress 配置
│   └── ConfigMap 和 Secrets
│
├── 3. 数据库迁移
│   ├── Prisma Migrate
│   ├── 自动化迁移脚本
│   └── 多环境管理
│
├── 4. 监控与日志
│   ├── ELK Stack
│   ├── Prometheus + Grafana
│   └── 日志收集与分析
│
└── 5. 性能优化
    ├── 连接池配置
    ├── 缓存策略
    └── 负载均衡

实践建议

  1. 对比实践:部署 NestJS 和 Nuxt3 项目,对比配置差异
  2. 问题模拟:故意删除某个 COPY 指令,观察错误
  3. 日志分析:配置日志映射,模拟错误查看日志
  4. 多环境配置:创建 .env.development 和 .env.production

附录:命令速查表

Dockerfile 关键指令

dockerfile
# 多阶段构建
FROM node:18-alpine AS builder
FROM node:18-alpine AS production

# 安全配置
USER node
WORKDIR /home/node/app
COPY --chown=node:node . .

# Prisma 相关
COPY prisma ./prisma/
RUN npm run generate
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma

# 生产依赖
RUN npm install --production
# 或
RUN npm install --omit=dev

常用命令

bash
# 构建镜像
docker build -t nest-app:1.0 .

# 运行容器
docker run -d --name nest-app -p 8000:3000 -e DATABASE_URL="xxx" nest-app:1.0

# Docker Compose
docker compose up -d          # 启动
docker compose down           # 停止
docker compose logs -f        # 查看日志
docker compose restart        # 重启
docker compose ps             # 查看状态

# 数据库操作
docker compose exec nest-app npx prisma migrate deploy  # 运行迁移
docker compose exec nest-app npx prisma studio          # 打开 Studio

# 调试命令
docker compose exec nest-app sh              # 进入容器
docker compose exec postgres psql -U postgres # 进入数据库
docker logs -f nest-app                      # 查看日志

环境变量模板

bash
# .env
DATABASE_URL="postgresql://postgres:password@postgres:5432/mydb"
JWT_SECRET="your-jwt-secret-key"
PORT=3000
NODE_ENV=production

# 可选
DB_USER=postgres
DB_PASSWORD=password
DB_NAME=mydb

下一步学习:CI/CD 自动化部署、Kubernetes 生产部署、数据库迁移自动化、监控告警系统集成。