티스토리 뷰

반응형

NestJS Swagger + Docker 배포 환경 구축 (Prisma까지 한 번에)

사실 여기까지 오면 “개발은 됐는데, 운영에선 깨지는” 디테일이 발목을 잡곤 한다.
이번 편은 NestJS 버전의 서비스를 대상으로 아래 3가지를 ‘한 번에’ 정리한다.

  1. Swagger(OpenAPI)로 문서/테스트 환경 붙이기
  2. Dockerfile + docker-compose로 MySQL/Redis/Nest 통합 실행
  3. Prisma와 런타임 배포가 정말 잘 맞물리게 만드는 요령

내가 실제로 겪은 시행착오(특히 Prisma migrate/Generate 타이밍, env 주입, 컨테이너 간 네트워킹)를 전제로 코드를 배치했다. 복붙해도 돌아가게 구성해두었다.


0) 준비 요약 (이전 글 기준)

  • NestJS 프로젝트(nest new), Prisma 연결/마이그레이션 완료
  • User, Post 모델 및 JWT 인증/CRUD 구현 완료
  • 패키지: @nestjs/jwt @nestjs/swagger swagger-ui-express prisma @prisma/client 등 설치됨

중요: Prisma CLI(prisma)는 컨테이너 안에서 migrate/deploy를 돌릴 수 있도록 devDependencies가 아니라 dependencies에 두는 게 편하다.
package.json에서 "dependencies"에 prisma, @prisma/client가 있도록 정리해두자.


1) Swagger 붙이기 (Nest 표준 구성)

설치

npm i @nestjs/swagger swagger-ui-express

main.ts

// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // 글로벌 파이프 (DTO 유효성)
  app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));

  // Swagger
  const config = new DocumentBuilder()
    .setTitle('Nest Blog API')
    .setDescription('NestJS + Prisma + MySQL API 문서')
    .setVersion('1.0.0')
    .addBearerAuth() // JWT Authorize 버튼
    .build();

  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('docs', app, document); // http://localhost:3000/docs

  await app.listen(process.env.PORT ? Number(process.env.PORT) : 3000);
  console.log(`API: http://localhost:${process.env.PORT ?? 3000}`);
  console.log(`Docs: http://localhost:${process.env.PORT ?? 3000}/docs`);
}
bootstrap();

간단한 DTO/컨트롤러 예 (스키마 노출)

반응형
// 예: src/post/dto/create-post.dto.ts
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';

export class CreatePostDto {
  @ApiProperty() @IsNotEmpty()
  title: string;

  @ApiProperty() @IsNotEmpty()
  content: string;
}
// 예: src/post/post.controller.ts (일부)
import { Body, Controller, Get, Post, UseGuards, Request } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/jwt.guard';
import { CreatePostDto } from './dto/create-post.dto';
import { PostService } from './post.service';

@ApiTags('Posts')
@Controller('posts')
export class PostController {
  constructor(private postService: PostService) {}

  @Get()
  getAll() {
    return this.postService.getAll();
  }

  @ApiBearerAuth()
  @UseGuards(JwtAuthGuard)
  @Post()
  create(@Request() req, @Body() dto: CreatePostDto) {
    return this.postService.create(req.user.id, dto.title, dto.content);
  }
}

2) Dockerfile (멀티스테이지, Prisma 대응)

포인트

  • 빌드 단계에서 prisma generate와 Nest build를 끝낸다.
  • 런타임 단계는 --omit=dev로 가볍게, 그러나 prisma CLI는 dependencies에 남겨 둔다.
# Dockerfile
# 빌드 스테이지
FROM node:20-alpine AS builder
WORKDIR /app

# 의존성 설치
COPY package*.json ./
RUN npm ci

# Prisma 스키마/클라이언트 생성
COPY prisma ./prisma
RUN npx prisma generate

# 소스 복사 & 빌드
COPY . .
RUN npm run build

# 런타임 스테이지
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production

# 프로덕션 의존성만
COPY package*.json ./
# prisma, @prisma/client가 dependencies에 있어야 함
RUN npm ci --omit=dev

# 빌드 산출물 & prisma 파일만 복사
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/prisma ./prisma

# 포트
EXPOSE 3000

# 컨테이너 시작 시: 마이그레이션(선택) 후 앱 실행
# 초기 세팅 시에는 migrate를 명시적으로 한 번 돌려주자 (아래 compose에서 처리)
CMD ["node", "dist/main.js"]

3) docker-compose.yml (MySQL, Redis, Nest 통합)

  • 컨테이너 간 통신은 localhost가 아니라 서비스명 사용
  • MySQL up 후에 앱이 뜨도록 healthcheck + depends_on 조건
  • 마이그레이션 1회성 Job을 분리해 확실하게 순서 보장 (권장)
# docker-compose.yml
version: "3.9"
services:
  app:
    build: .
    container_name: nest_app
    ports:
      - "3000:3000"
    env_file:
      - ./.env
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_started
    restart: always
    # 마이그레이션을 migrate 서비스에서 처리하는 전략을 권장
    command: ["node", "dist/main.js"]
    networks: [ backend ]

  migrate:
    build: .
    container_name: nest_migrate
    env_file:
      - ./.env
    depends_on:
      mysql:
        condition: service_healthy
    # prisma가 dependencies에 있어야 아래가 동작한다
    command: ["npx", "prisma", "migrate", "deploy"]
    networks: [ backend ]
    # 한 번 성공하면 자동 종료
    restart: "no"

  mysql:
    image: mysql:8.0
    container_name: mysql8
    environment:
      MYSQL_ROOT_PASSWORD: 1234
      MYSQL_DATABASE: testdb
      MYSQL_USER: appuser
      MYSQL_PASSWORD: 1234
    ports:
      - "3306:3306"
    healthcheck:
      test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -p1234 --silent"]
      interval: 5s
      timeout: 5s
      retries: 30
    volumes:
      - db_data:/var/lib/mysql
    networks: [ backend ]
    restart: always

  redis:
    image: redis:7
    container_name: redis7
    ports:
      - "6379:6379"
    networks: [ backend ]
    restart: always

networks:
  backend:

volumes:
  db_data:

실행 순서:

  1. docker compose up -d mysql redis (DB/Redis 준비)
  2. docker compose run --rm migrate (스키마 반영)
  3. docker compose up -d app (애플리케이션 기동)

한 번 세팅 후에는 보통 compose up -d만으로 충분하다.
스키마 변경이 있을 때만 migrate 서비스를 다시 한 번 실행해주면 된다.


4) .env (Prisma의 DATABASE_URL과 서비스명 주의)

컨테이너 네트워킹에서는 DB_HOST=localhost가 아니다. mysql 서비스명을 써야 한다.

# .env
PORT=3000
JWT_SECRET=my_secret_key

# compose 내부 네트워크 기준
DB_HOST=mysql
DB_PORT=3306
DB_USER=appuser
DB_PASSWORD=1234
DB_NAME=testdb

# Prisma 전용: 반드시 서비스명을 사용하여 URL 구성
DATABASE_URL="mysql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}"

# Redis
REDIS_HOST=redis
REDIS_PORT=6379

@prisma/client는 DATABASE_URL을 바라본다.
이 값이 컨테이너 내부에서도 정확히 가리키도록 반드시 서비스명(mysql) 으로 작성해야 한다.


5) 실행 명령 요약

# 1) 이미지 빌드
docker compose build

# 2) DB/Redis 먼저
docker compose up -d mysql redis

# 3) 마이그레이션 1회 적용
docker compose run --rm migrate

# 4) 앱 실행
docker compose up -d app

# 확인
curl http://localhost:3000/docs
  • Swagger UI에서 Authorize 버튼 → Bearer <JWT> 토큰 입력 후 보호 API 바로 테스트 가능
  • Prisma Client는 빌드 단계에서 generate가 끝나 있기 때문에 런타임에서 바로 사용된다.

6) Blue-Green/Nginx/PM2와의 연결 (선택)

이전 Express 편의 무중단 배포 구조를 그대로 가져가려면:

  • app의 외부 포트를 3001(blue)/3002(green) 로 나누고
  • Nginx upstream을 해당 포트로 번갈아 바라보게 하면 끝
  • PM2를 쓰고 싶다면 Nest도 node dist/main.js 대신 PM2 cluster로 실행 가능
    (컨테이너 내부에서는 보통 단일 프로세스를 권장. 호스트/오케스트레이터 레벨에서 스케일링)

7) 현업에서 자주 나는 문제 체크리스트

  • Prisma CLI 누락: 컨테이너에서 npx prisma가 안 되면 prisma가 devDeps에만 있는 것. dependencies로 옮기자.
  • DATABASE_URL 오타: localhost 쓰면 100% 실패. 서비스명(mysql) 필수.
  • migrate 타이밍: 앱 시작 전에 스키마가 안 올라오면 에러. 위처럼 migrate Job을 분리해 순서 보장.
  • healthcheck 부족: MySQL이 살아도 “접속 가능”이 아닐 수 있다. mysqladmin ping으로 확인.
  • Swagger 미노출: SwaggerModule.setup('docs', ...) 경로 확인. 프록시(Nginx) 뒤라면 path 재확인.

출처

  • NestJS 공식 문서 (Swagger, Pipes, Guards, Module 구조)
  • Swagger(OpenAPI) - @nestjs/swagger, swagger-ui-express
  • Prisma Docs (Migrate, Generate, DATABASE_URL)
  • Docker/Docker Compose 공식 문서 (healthcheck, depends_on 조건)

 

NestJS, TypeScript, Swagger, OpenAPI, Prisma, MySQL, Redis, Docker, DockerCompose, 백엔드배포

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/01   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함
반응형