{T}

NestJS 容器化部署实战

概述

NestJS 后端服务的容器化与前端 SSR 项目有显著差异:需要处理 Prisma Client 生成、生产依赖安装、数据库连接配置和日志持久化。本文讲解 NestJS 应用从 Dockerfile 编写到 Docker Compose 多服务编排的完整部署方案。

前置知识

学习目标

  1. 理解 NestJS 与 Nuxt3 容器化的核心差异
  2. 掌握含 Prisma 的 NestJS Dockerfile 编写
  3. 熟练配置数据库连接与日志 Volume 映射
  4. 能够搭建 Docker Compose 多服务生产环境

一、NestJS vs Nuxt3 容器化差异

维度NestJS(后端 API)Nuxt3(SSR 前端)
构建产物dist/ 目录.output/ 目录
运行时依赖需要 npm install --omit=dev已打包在 .output 中
Prisma需要生成 + 拷贝 Client通常不需要
数据库必须配置连接通常通过 API 间接访问
端口3000(API 服务)3000(Web 服务)
日志需要持久化映射相对轻量

NestJS 容器化特殊需求

图表渲染中…

二、Dockerfile 完整配置

package.json 调整

json
{
  "scripts": {
    "generate": "prisma generate"
  }
}

preinstall: "prisma generate" 改为手动脚本,避免构建阶段自动执行导致失败。

完整 Dockerfile

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

USER node
WORKDIR /home/node/app

RUN npm config set registry https://registry.npmmirror.com

# 复制依赖配置和 Prisma Schema
COPY --chown=node:node package*.json ./
COPY --chown=node:node prisma ./prisma/

# 安装全量依赖
RUN npm install

# 生成 Prisma Client
RUN npx prisma generate

# 复制源码并构建
COPY --chown=node:node . .
RUN npm run build

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

USER node
WORKDIR /home/node/app

# 安装生产依赖
COPY --from=builder --chown=node:node /home/node/app/package*.json ./
RUN 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"]

关键步骤解析

为什么必须手动拷贝 Prisma Client?

  • npm install --omit=dev 不安装 Prisma CLI(开发依赖)
  • 但运行时需要 @prisma/client.prisma/client(生成的类型)
  • 必须从构建阶段拷贝这两个目录

文件拷贝顺序的重要性:

  1. package.json → 定义依赖
  2. npm install --omit=dev → 安装生产依赖
  3. Prisma Client → 覆盖/补充到 node_modules
  4. dist/ → 最后拷贝构建产物

.dockerignore

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

三、环境变量与数据库配置

Docker Compose + .env(推荐)

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

注意:DATABASE_URL 中的 host 使用 Docker 服务名 postgres(而非 localhost),Docker 内部 DNS 会自动解析。

命令行传递

bash
docker run -d --name nest-app -p 3000:3000 \
  -e DATABASE_URL="postgresql://user:pass@host:5432/db" \
  -e JWT_SECRET="secret" \
  nest-app:1.0

四、日志映射与持久化

为什么需要日志映射

容器删除后内部文件丢失,日志必须通过 Volume 持久化到宿主机。

Volume 配置

yaml
services:
  nest-app:
    image: nest-app:1.0
    volumes:
      - ./logs:/home/node/app/logs    # 日志目录映射

NestJS 日志配置(Winston 示例):

typescript
// 确保日志输出到文件
const logger = winston.createLogger({
  transports: [
    new winston.transports.File({
      filename: 'logs/error.log',
      level: 'error',
    }),
    new winston.transports.File({
      filename: 'logs/combined.log',
    }),
  ],
});

五、Docker Compose 多服务编排

完整生产配置

yaml
version: '3.8'

services:
  # PostgreSQL 数据库
  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
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Redis 缓存
  redis:
    image: redis:7-alpine
    container_name: redis-cache
    restart: always
    ports:
      - "6379:6379"
    networks:
      - app-network

  # NestJS 应用
  nest-app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: nest-app-prod
    restart: always
    ports:
      - "3000:3000"
    env_file:
      - .env
    volumes:
      - ./logs:/home/node/app/logs
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
    networks:
      - app-network

volumes:
  postgres-data:

networks:
  app-network:
    driver: bridge

depends_on 与健康检查

yaml
depends_on:
  postgres:
    condition: service_healthy   # 等待数据库健康检查通过
  redis:
    condition: service_started   # 只需启动即可

这确保 NestJS 应用不会在数据库就绪前启动,避免连接失败。


六、完整部署流程

bash
# 1. 构建镜像
docker compose build

# 2. 启动所有服务
docker compose up -d

# 3. 执行数据库迁移
docker compose exec nest-app npx prisma migrate deploy

# 4. 填充初始数据(可选)
docker compose exec nest-app npx prisma db seed

# 5. 查看运行状态
docker compose ps
docker compose logs -f nest-app

# 6. 更新部署
git pull
docker compose build
docker compose up -d
docker compose exec nest-app npx prisma migrate deploy

常用运维命令

bash
docker compose ps                         # 服务状态
docker compose logs -f nest-app           # 实时日志
docker compose exec nest-app sh           # 进入容器
docker compose restart nest-app           # 重启应用
docker compose down                       # 停止所有
docker compose down -v                    # 停止并删除数据卷
docker compose exec postgres psql -U postgres -d mydb  # 连接数据库

常见问题

数据库连接失败

原因解决方案
host 使用 localhost改为 Docker 服务名(如 postgres
数据库未就绪配置 depends_on + healthcheck
密码/端口错误检查 .env 与 postgres 服务配置一致
网络不通确保在同一 networks

Prisma Client 找不到

bash
# 确认构建阶段执行了 prisma generate
# 确认生产阶段拷贝了 .prisma 和 @prisma 目录
docker compose exec nest-app ls node_modules/.prisma/client

容器启动即退出

bash
docker compose logs nest-app   # 查看错误
# 常见原因:dist/main.js 路径错误、环境变量缺失、端口冲突

最佳实践

安全配置清单

项目要求
用户非 root(USER node)
文件权限COPY --chown=node:node
敏感配置env_file 注入,.env 加入 .gitignore
数据库密码不使用默认密码,生产用 Docker Secret
网络内部服务不暴露不必要端口

性能优化

  • 使用 alpine 镜像减小体积
  • 多阶段构建排除开发依赖
  • PostgreSQL 数据使用 named volume 持久化
  • 配置 restart: always 确保异常自动恢复
  • 日志文件定期轮转,避免磁盘占满

监控与告警

yaml
# 健康检查配置
healthcheck:
  test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 15s

延伸阅读


上一篇:Docker 安全性与命令实践指南