티스토리 뷰
반응형
NestJS Swagger + Docker 배포 환경 구축 (Prisma까지 한 번에)
사실 여기까지 오면 “개발은 됐는데, 운영에선 깨지는” 디테일이 발목을 잡곤 한다.
이번 편은 NestJS 버전의 서비스를 대상으로 아래 3가지를 ‘한 번에’ 정리한다.
- Swagger(OpenAPI)로 문서/테스트 환경 붙이기
- Dockerfile + docker-compose로 MySQL/Redis/Nest 통합 실행
- 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:
실행 순서:
- docker compose up -d mysql redis (DB/Redis 준비)
- docker compose run --rm migrate (스키마 반영)
- 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, 백엔드배포
'study > 백엔드' 카테고리의 다른 글
| GitHub Actions로 완전 자동화된 NestJS 배포 파이프라인 구축하기 (0) | 2025.11.17 |
|---|---|
| NestJS + Prisma + Redis + PM2 운영환경 완성편 (0) | 2025.11.04 |
| NestJS + Prisma로 JWT 인증 & 게시글 CRUD 구현하기 (0) | 2025.10.30 |
| NestJS + TypeScript로 리팩토링하기 (Express 프로젝트 진화편 1) (0) | 2025.10.29 |
| Express + MySQL 사이드프로젝트 아키텍처 완성편 (0) | 2025.10.28 |
※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- DevOps
- Python
- JAX
- Express
- Docker
- llm
- PostgreSQL
- 웹개발
- nextJS
- 쿠버네티스
- kotlin
- CI/CD
- ai철학
- 백엔드개발
- fastapi
- Redis
- JWT
- SEO최적화
- node.js
- flax
- 개발블로그
- 압박면접
- 딥러닝
- Prisma
- Next.js
- NestJS
- REACT
- 프론트엔드개발
- rag
- seo 최적화 10개
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
글 보관함
반응형

