프로젝트 배포를 위해 Docker 이미지를 빌드하고 확인한 순간, 경악을 금치 못했습니다. 이미지 크기가 무려 1.5GB였기 때문입니다.
용량이 커지면 GitHub Actions의 배포 파이프라인 속도가 느려지고, 이는 곧 개발 생산성 저하로 이어집니다.
이를 해결하기 위해 단계별로 최적화를 진행한 과정을 공유합니다.
1. 배경 및 문제 파악 (Context & Problem)
CI/CD 파이프라인을 구축하면서 Docker 이미지를 생성했는데, 빌드 속도와 푸시 속도가 눈에 띄게 느렸습니다.
문제: Docker 이미지 최종 크기 1.5GB.
원인: 초기 빌드 시
devDependencies와dependencies가 모두 이미지에 포함되었고, Node.js의 거대한node_modules가 그대로 레이어에 쌓이고 있었습니다.
2. 해결 과정 (Solution)
Step 1: --prod 설치로 불필요한 의존성 제거
가장 먼저 실행 단계에서 불필요한 devDependencies를 제외했습니다.
조치:
pnpm install --prod명령어를 통해 실행에 꼭 필요한 라이브러리만 남겼습니다.결과: 1.5GB → 1.0GB (약 500MB 절감)
Step 2: Prisma Client 복사의 늪 탈출하기
NestJS와 Prisma를 함께 사용할 때 가장 까다로운 부분은 Prisma Client의 위치였습니다.
문제: 심볼릭 링크의 한계와 용량의 딜레마
pnpm은 효율적인 용량 관리를 위해
node_modules내부를 심볼릭 링크(Symbolic Link)로 구성합니다. 이로 인해 Docker 빌드 과정에서 두 가지 문제가 발생했습니다복사 시 참조 깨짐:
prisma generate로 생성된 파일이 실제로는.pnpm폴더 내부에 존재하고node_modules/@prisma는 이를 가리키는 링크일 뿐이었습니다. 컨테이너로 복사 시 이 연결이 깨지며 런타임 오류가 발생했습니다.용량 최적화 실패: 이 오류를 피하려면
.pnpm을 포함한node_modules전체를 복사해야 했고, 이는 이미지 크기를 비대하게 만들었습니다.
해결: 커스텀 Output과 호이스팅 전략
문제를 근본적으로 해결하기 위해 생성 위치 강제 지정과 의존성 구조 변경을 동시에 적용했습니다.
커스텀 Output 설정:
schema.prisma파일에서 Client 생성 경로를@prisma/client대신 프로젝트 루트의.prisma경로로 커스텀 설정했습니다. 이를 통해 Docker 빌드 시node_modules전체 대신, 독립된.prisma폴더만 선택적으로 레이어에 포함시켜 용량을 대폭 줄였습니다.shamefully-hoist 적용: pnpm의 엄격한 의존성 격리 때문에 런타임에서 Client를 찾지 못하는 문제를 해결하기 위해,
--shamefully-hoist설정을 사용했습니다. 의존성을 루트node_modules로 호이스팅하여 애플리케이션이 어떤 환경에서도 Prisma Client를 안정적으로 참조할 수 있게 했습니다.
결과: 1.0GB → 0.79GB
Step 3: Corepack 도입으로 패키지 매니저 최적화
Docker 이미지 내부에서 pnpm을 설치하는 대신, Node.js에 내장된 Corepack을 활용했습니다.
조치:
corepack enable을 통해 별도의 pnpm 설치 과정 없이 패키지 매니저를 활성화했습니다.결과: 0.79GB → 0.78GB (미세하지만 환경 설정의 깔끔함과 용량 이득 확보)
3. 회고 (Lesson Learned)
결과적으로 이미지 크기를 약 44% 절감했습니다. 수치적인 이득뿐만 아니라, GitHub Actions에서 이미지를 빌드하고 오라클 서버로 푸시하는 전체적인 파이프라인 속도가 눈에 띄게 빨라져 매우 만족스럽습니다.
특히 pnpm 환경에서 Prisma의 심볼릭 링크 구조가 Docker 빌드 시 문제를 일으킬 수 있다는 점을 깊게 이해하게 된 계기였습니다. 앞으로는 초기 설계 단계부터 이미지 최적화를 고려하는 습관을 가져야겠습니다.
prisma.schema 파일
generator client {
provider = "prisma-client-js"
output = "../../node_modules/.prisma/client"
}
datasource db {
provider = "postgresql"
}
최종 도커 파일
# ---- Build Stage ----
FROM node:20-alpine AS builder
RUN corepack enable pnpm
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm prisma generate
RUN pnpm build
# ---- Production Stage ----
FROM node:20-alpine AS runner
RUN corepack enable pnpm
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --shamefully-hoist --prod --frozen-lockfile
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
EXPOSE 4000
CMD ["pnpm", "start:prod"]