콘텐츠로 건너뛰기

Dockerfile 용량 줄이는 법: 멀티 스테이지 빌드와 캐시 최적화

핵심 요약

  • 이미지 용량이 크면 배포·다운로드가 느리고, 저장 비용과 공격 표면이 늘어납니다.
  • 멀티 스테이지 빌드: 빌드에 필요한 도구는 빌드 단계에서만 쓰고, 최종 이미지에는 결과물만 복사해 크기를 크게 줄입니다.
  • 캐시 최적화: 잘 안 바뀌는 것(의존성 설치)을 위에, 자주 바뀌는 것(소스 코드)을 아래에 두어 빌드 캐시를 최대한 재사용합니다.
  • 경량 베이스 이미지(alpine·slim·distroless)와 .dockerignore로 불필요한 용량을 더 덜어냅니다.

Docker로 처음 앱을 이미지로 만들어 보면 “고작 이 앱 하나가 1GB가 넘는다고?” 하고 놀라게 됩니다. 이미지가 크면 배포할 때마다 내려받는 시간이 길어지고, 레지스트리 저장 비용과 보안 점검 대상도 늘어납니다. 다행히 Dockerfile을 몇 가지 원칙만 지켜 다시 쓰면 이미지 용량을 크게 줄일 수 있습니다. 이 글에서는 멀티 스테이지 빌드, 캐시 최적화, 경량 베이스 이미지라는 세 가지 핵심을 정리합니다.

이미지는 ‘레이어’로 쌓이고, 캐시된다

Dockerfile의 각 명령(FROM·RUN·COPY 등)은 하나의 레이어(layer)를 만듭니다. Docker는 이 레이어들을 캐시해 두었다가, 바뀌지 않은 레이어는 다시 빌드하지 않고 재사용합니다. 그런데 어떤 레이어가 바뀌면 그 아래 모든 레이어의 캐시가 무효화됩니다. 그래서 “무엇을 위에 두고 무엇을 아래에 두느냐”가 빌드 속도와 캐시 효율을 좌우합니다.

멀티 스테이지 빌드: 결과물만 남긴다

이미지가 뚱뚱해지는 가장 큰 원인은 빌드에만 필요한 도구가 최종 이미지에 그대로 남는 것입니다. 컴파일러, 빌드 캐시, 개발 의존성 등은 앱을 실행할 때는 필요 없는데도 함께 딸려 옵니다. 멀티 스테이지 빌드는 빌드 단계와 실행 단계를 나눠, 빌드 단계에서 만든 산출물만 가벼운 실행 이미지로 복사합니다.

빌드 스테이지 (build) 컴파일러·개발 의존성 소스 → 빌드 산출물 무겁다 (약 1GB+) COPY –from=build 산출물만 복사 런타임 스테이지 (final) 경량 베이스 + 산출물 가볍다 (수십 MB)
빌드 도구는 빌드 스테이지에 두고, 최종 이미지에는 실행에 필요한 산출물만 담습니다.
# 1) 빌드 스테이지: 무거운 빌드 도구 사용
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# 2) 런타임 스테이지: 가벼운 이미지 + 산출물만 복사
FROM node:20-slim
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]

핵심은 COPY --from=build입니다. 무거운 node:20 빌드 이미지에서 만든 dist만 가벼운 node:20-slim으로 가져오면, 최종 이미지에는 컴파일 과정의 잔여물이 남지 않습니다.

캐시 최적화: 순서만 바꿔도 빌드가 빨라진다

가장 흔한 실수는 소스 전체를 먼저 복사한 뒤 의존성을 설치하는 것입니다. 이러면 코드를 한 줄만 고쳐도 COPY . . 아래의 의존성 설치 레이어 캐시가 매번 깨져, 빌드할 때마다 패키지를 새로 내려받습니다. 반대로 의존성 파일을 먼저 복사·설치하고 소스는 그 뒤에 복사하면, 소스만 바뀔 때 무거운 설치 단계는 캐시로 건너뜁니다.

비효율 (캐시 자주 깨짐)권장 (캐시 재사용)
순서COPY . . → 의존성 설치의존성 파일 COPY → 설치 → 소스 COPY
코드 한 줄 수정 시의존성 매번 재설치설치 건너뜀(캐시)

경량 베이스 이미지와 .dockerignore

베이스 이미지 선택만으로도 용량 차이가 큽니다. 일반 이미지(예: python:3.12) 대신 slim이나 alpine 계열을 쓰면 수백 MB를 줄일 수 있고, 실행 환경만 담은 distroless는 더 작고 안전합니다. 다만 alpine은 일부 C 라이브러리 호환 문제가 있을 수 있어, 앱에 맞춰 검증이 필요합니다. 여기에 .dockerignore 파일로 node_modules·.git·로그 같은 불필요한 파일이 빌드 컨텍스트에 들어가지 않게 막으면, 빌드도 빨라지고 이미지도 깔끔해집니다.

직접 해보며 느낀 점 (신입 관점)

처음 만든 이미지가 1GB를 훌쩍 넘겨서 “왜 이렇게 크지?” 했다가, 멀티 스테이지 빌드와 slim 베이스로 바꾸니 수십 MB로 줄어드는 걸 보면 원리가 확 와닿습니다. 결국 이미지 최적화는 “실행에 진짜 필요한 것만 남긴다”는 한 가지 원칙의 반복입니다. 신입 인프라·백엔드 면접에서도 “이미지 용량을 어떻게 줄이나요?”는 자주 나오는데, 멀티 스테이지 + 캐시 순서 + 경량 베이스 세 가지를 실제 Dockerfile과 함께 말할 수 있으면 충분합니다.

자주 묻는 질문 (FAQ)

Q. 멀티 스테이지 빌드는 언제 꼭 필요한가요?
컴파일·번들링 같은 빌드 과정이 있는 앱(예: Go, Java, 프론트엔드 번들, TypeScript)에서 특히 효과가 큽니다. 빌드 도구를 최종 이미지에서 완전히 분리할 수 있기 때문입니다.

Q. RUN 명령을 여러 개로 나누면 안 좋은가요?
각 RUN이 레이어를 만들기 때문에, 관련 있는 명령은 &&로 묶고 설치 후 캐시·임시 파일을 같은 레이어에서 정리하면 용량을 줄일 수 있습니다.

Q. alpine을 쓰면 무조건 좋은가요?
대개 가장 작지만, alpine은 musl libc를 써서 일부 패키지에서 호환 문제가 생길 수 있습니다. 그럴 땐 slim 계열이 안전한 절충안입니다.

마무리

이미지 최적화의 큰 그림은 세 가지입니다. 멀티 스테이지로 빌드 도구를 떼어내고, 캐시가 잘 먹도록 순서를 잡고, 경량 베이스와 .dockerignore로 군더더기를 덜어낸다. 이 원칙만 지켜도 배포 속도와 저장 비용, 보안 표면이 눈에 띄게 개선됩니다. 다음 단계로는 여러 컨테이너를 묶어 실행하는 Docker Compose로 넘어가면 좋습니다.

더 읽어보기

이 글은 학습·정보 제공을 목적으로 작성되었습니다. 실제 환경과 Docker 버전, 언어 스택에 따라 최적화 방법은 달라질 수 있습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다