[TroubleShooting] 1.5GB에서 0.84GB까지, Docker 이미지 44% 최적화하기

프로젝트 배포를 위해 Docker 이미지를 빌드하고 확인한 순간, 경악을 금치 못했습니다. 이미지 크기가 무려 1.5GB였기 때문입니다.

용량이 커지면 GitHub Actions의 배포 파이프라인 속도가 느려지고, 이는 곧 개발 생산성 저하로 이어집니다.

이를 해결하기 위해 단계별로 최적화를 진행한 과정을 공유합니다.

1. 배경 및 문제 파악 (Context & Problem)

CI/CD 파이프라인을 구축하면서 Docker 이미지를 생성했는데, 빌드 속도와 푸시 속도가 눈에 띄게 느렸습니다.

  • 문제: Docker 이미지 최종 크기 1.5GB.

  • 원인: 초기 빌드 시 devDependencies와 dependencies가 모두 이미지에 포함되었고, Node.js의 거대한 node_modules가 그대로 레이어에 쌓이고 있었습니다.

스크린샷 2026-04-04 오후 5.19.19

2. 해결 과정 (Solution)

Step 1: --prod 설치로 불필요한 의존성 제거

가장 먼저 실행 단계에서 불필요한 devDependencies를 제외했습니다.

  • 조치: pnpm install --prod 명령어를 통해 실행에 꼭 필요한 라이브러리만 남겼습니다.

  • 결과: 1.5GB → 1.0GB (약 500MB 절감)

스크린샷 2026-04-04 오후 5.12.06

Step 2: Prisma Client 복사의 늪 탈출하기

NestJS와 Prisma를 함께 사용할 때 가장 까다로운 부분은 Prisma Client의 위치였습니다.

  • 문제: 심볼릭 링크의 한계와 용량의 딜레마

  • pnpm은 효율적인 용량 관리를 위해 node_modules 내부를 심볼릭 링크(Symbolic Link)로 구성합니다. 이로 인해 Docker 빌드 과정에서 두 가지 문제가 발생했습니다

    1. 복사 시 참조 깨짐: prisma generate로 생성된 파일이 실제로는 .pnpm 폴더 내부에 존재하고 node_modules/@prisma는 이를 가리키는 링크일 뿐이었습니다. 컨테이너로 복사 시 이 연결이 깨지며 런타임 오류가 발생했습니다.

    2. 용량 최적화 실패: 이 오류를 피하려면 .pnpm을 포함한 node_modules 전체를 복사해야 했고, 이는 이미지 크기를 비대하게 만들었습니다.

  • 해결: 커스텀 Output과 호이스팅 전략

  • 문제를 근본적으로 해결하기 위해 생성 위치 강제 지정의존성 구조 변경을 동시에 적용했습니다.

    1. 커스텀 Output 설정: schema.prisma 파일에서 Client 생성 경로를 @prisma/client 대신 프로젝트 루트의 .prisma 경로로 커스텀 설정했습니다. 이를 통해 Docker 빌드 시 node_modules 전체 대신, 독립된 .prisma 폴더만 선택적으로 레이어에 포함시켜 용량을 대폭 줄였습니다.

    2. shamefully-hoist 적용: pnpm의 엄격한 의존성 격리 때문에 런타임에서 Client를 찾지 못하는 문제를 해결하기 위해, --shamefully-hoist 설정을 사용했습니다. 의존성을 루트 node_modules로 호이스팅하여 애플리케이션이 어떤 환경에서도 Prisma Client를 안정적으로 참조할 수 있게 했습니다.

  • 결과: 1.0GB → 0.79GB

스크린샷 2026-04-04 오후 5.19.04

Step 3: Corepack 도입으로 패키지 매니저 최적화

Docker 이미지 내부에서 pnpm을 설치하는 대신, Node.js에 내장된 Corepack을 활용했습니다.

  • 조치: corepack enable을 통해 별도의 pnpm 설치 과정 없이 패키지 매니저를 활성화했습니다.

  • 결과: 0.79GB → 0.78GB (미세하지만 환경 설정의 깔끔함과 용량 이득 확보)

스크린샷 2026-04-04 오후 5.18.31

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"]