본문 바로가기
클라우드/쿠버네티스

🚀 Dockerfile 다이어트 비법: 가볍고 빠른 이미지를 위한 멀티스테이지 빌드 전략

by gasbugs 2025. 9. 2.

안녕하세요! 👋 오늘은 Docker를 사용하시는 분들이라면 누구나 한 번쯤 고민해봤을 주제, "어떻게 하면 Docker 이미지를 더 작고 효율적으로 만들 수 있을까?" 에 대한 해답을 들고 왔습니다.

 

느린 빌드 시간, 거대한 이미지 크기, 불필요한 파일로 인한 보안 취약점... 😥 이런 문제들로 골머리를 앓고 계셨다면 오늘 포스팅에 주목해주세요. 특히, Dockerfile 작성의 '치트키'라 불리는 멀티스테이지 빌드(Multi-stage builds) 전략을 중심으로 효율적인 Dockerfile 작성법을 A to Z까지 상세히 알려드릴게요!

 

 


🤔 왜 Docker 이미지 크기가 중요한가요?

"일단 돌아가기만 하면 되는 거 아닌가?"라고 생각할 수도 있지만, 이미지 크기는 생각보다 많은 것에 영향을 미칩니다.

  • ⚡ 빠른 배포: 이미지 크기가 작을수록 레지스트리(Docker Hub 등)에 push하고 pull하는 속도가 빨라져 CI/CD 파이프라인 전체의 시간이 단축됩니다.
  • 💰 비용 절감: 작은 이미지는 레지스트리 저장 공간과 네트워크 트래픽 비용을 줄여줍니다.
  • 🔒 보안 강화: 이미지에 빌드 도구나 소스코드 등 실행에 불필요한 파일이 포함되지 않으므로 공격에 노출될 지점(Attack Surface)이 줄어듭니다.

효율적인 Dockerfile 작성은 단순히 '용량 다이어트'를 넘어, 더 빠르고, 저렴하며, 안전한 애플리케이션 환경을 만드는 핵심 첫걸음입니다.


🛠️ 기본 다지기: 효율적인 Dockerfile 작성 전략

멀티스테이지 빌드를 알아보기 전에, 기본적으로 지켜야 할 몇 가지 중요한 전략들이 있습니다. 이것들만 잘 지켜도 이미지 크기를 상당히 줄일 수 있답니다!

1. 가벼운 기반 이미지(Base Image) 선택하기 📦

모든 Docker 이미지는 FROM 명령어로 시작하죠. 이때 어떤 기반 이미지를 선택하느냐가 전체 크기를 좌우합니다.

  • 피해야 할 이미지: ubuntu, centos 등 일반적인 OS 이미지는 편리하지만 각종 시스템 유틸리티가 포함되어 있어 매우 큽니다.
  • 추천하는 이미지:
    • alpine: 초경량 리눅스 배포판으로, 최소한의 기능만 포함하고 있어 매우 인기가 높습니다. (보통 5~10MB 수준)
    • slim: 각 언어별 공식 이미지에서 제공하는 경량화 버전입니다. (python:3.9-slim)
    • distroless: 'Distro-less(배포판 없음)'라는 이름처럼, 셸이나 패키지 관리자조차 없는, 오직 애플리케이션 실행에 필요한 최소한의 라이브러리만 포함된 구글의 이미지입니다. 보안에 특히 강력합니다.

2. 레이어(Layer) 개수 최소화하기 겹겹이 쌓지 마세요! 🍔

Dockerfile의 RUN, COPY, ADD 명령어는 각각 하나의 새로운 이미지 레이어를 만듭니다. 레이어가 많아질수록 이미지 크기는 커집니다. 여러 RUN 명령어를 && 연산자로 연결하여 하나의 레이어로 합치는 것이 좋습니다.

 

나쁜 예 👎

Dockerfile
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

좋은 예 👍

Dockerfile
RUN apt-get update && \
    apt-get install -y curl && \
    rm -rf /var/lib/apt/lists/*

Tip: 패키지 설치 후 캐시 파일(rm -rf /var/lib/apt/lists/*)을 같은 RUN 명령어 내에서 삭제해야 해당 레이어에서 실제로 파일이 제거되어 용량이 줄어듭니다.

3. 빌드 캐시(Build Cache) 적극 활용하기 ⚡

Docker는 이미지 빌드 시 각 명령어의 결과를 캐싱해 둡니다. 다음 빌드 시 파일 내용이나 명령어가 변경되지 않았다면, 이전 레이어의 캐시를 그대로 사용하여 빌드 속도를 획기적으로 높입니다.

이 캐시를 잘 활용하려면 변경 빈도가 낮은 명령어부터 높은 순서로 Dockerfile을 작성해야 합니다.

 

나쁜 예 👎 (소스코드가 바뀔 때마다 매번 의존성 설치)

Dockerfile
COPY . . # 소스코드 먼저 복사
RUN npm install # 의존성 설치
CMD ["npm", "start"]

 

좋은 예 👍 (소스코드가 바뀌어도 캐시된 의존성 레이어 재사용)

Dockerfile
WORKDIR /app
COPY package*.json ./ # 1. package.json 먼저 복사 (변경 빈도 낮음)
RUN npm install # 2. 의존성 설치
COPY . . # 3. 나머지 소스코드 복사 (변경 빈도 높음)
CMD ["npm", "start"]

4. .dockerignore 파일 사용은 필수! 🗑️

프로젝트 루트에 .dockerignore 파일을 만들면 .gitignore처럼 Docker 빌드 컨텍스트에 포함되지 않을 파일과 디렉터리를 지정할 수 있습니다. node_modules, build, .git, 각종 로그 파일 등 불필요한 파일이 이미지에 복사되는 것을 막아줍니다.

코드 스니펫
.git
.vscode
node_modules
npm-debug.log
README.md

🚀 최강의 무기: 멀티스테이지 빌드 (Multi-stage Builds)

자, 이제 오늘의 주인공인 멀티스테이지 빌드에 대해 알아볼 시간입니다!

이 전략은 컴파일이 필요한 언어(Go, Java, C++, Rust 등)나 빌드 과정이 복잡한 프론트엔드 애플리케이션(React, Vue)에서 특히 강력한 힘을 발휘합니다.

멀티스테이지 빌드란?

하나의 Dockerfile 안에 여러 개의 FROM 문을 사용하여 빌드 단계를 여러 개로 나누는 기술입니다.

  1. 빌드(Build) 스테이지: 첫 번째 스테이지에서는 컴파일러, 빌드 도구, SDK 등 모든 개발 의존성을 포함한 무거운 이미지를 기반으로 소스코드를 컴파일하거나 빌드합니다.
  2. 최종(Final) 스테이지: 두 번째 스테이지에서는 아주 가벼운 실행 전용 이미지(예: alpine)를 기반으로 시작합니다. 그리고 COPY --from 명령어를 사용하여 이전 빌드 스테이지에서 생성된 결과물(실행 파일, 빌드된 정적 파일 등)만 쏙 가져옵니다.

결과적으로, 최종 이미지에는 무거운 빌드 도구나 소스코드는 전혀 남지 않고, 오직 애플리케이션 실행에 필요한 최소한의 파일만 남게 됩니다!

 

예시: Go 애플리케이션 빌드하기

멀티스테이지 빌드 적용 전 👎

Dockerfile
# Go SDK가 포함된 무거운 이미지를 사용
FROM golang:1.19

WORKDIR /app

# 소스코드와 의존성 파일 복사
COPY go.mod ./
COPY go.sum ./
RUN go mod download

COPY *.go ./

# 애플리케이션 빌드
RUN go build -o /app/main .

# 이 이미지에는 Go 컴파일러, 소스코드 등 불필요한 파일이 모두 남아있음
# 이미지 크기: 수백 MB ~ 1GB 이상
CMD [ "/app/main" ]

멀티스테이지 빌드 적용 후 👍

Dockerfile
# ========== 1. 빌드 스테이지 ==========
# 'builder'라는 이름으로 빌드 스테이지를 정의
FROM golang:1.19 AS builder

WORKDIR /app

COPY go.mod ./
COPY go.sum ./
RUN go mod download

COPY *.go ./

# Go 애플리케이션을 정적 바이너리로 빌드
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main .

# ========== 2. 최종 스테이지 ==========
# 아주 작은 scratch 이미지나 alpine 이미지를 기반으로 시작
FROM alpine:latest

WORKDIR /root/

# 'builder' 스테이지의 /app/main 파일을 현재 스테이지로 복사
COPY --from=builder /app/main .

# 최종 이미지에는 오직 'main' 실행 파일 하나만 존재!
# 이미지 크기: 10~20MB 수준으로 급격히 감소
CMD ["./main"]

어떤가요? COPY --from=builder 이 한 줄의 마법으로 이미지 크기가 수십 분의 일로 줄어드는 것을 볼 수 있습니다. ✨

이 전략은 Java(JAR 파일만 복사), React(빌드된 정적 파일만 Nginx 이미지로 복사) 등 거의 모든 애플리케이션에 동일하게 적용할 수 있습니다.


✨ 정리하며

오늘 우리는 더 작고, 빠르고, 안전한 Docker 이미지를 만들기 위한 다양한 전략을 살펴봤습니다.

  1. 가벼운 기반 이미지(alpine, distroless)를 선택하세요.
  2. &&를 사용해 레이어를 최소화하세요.
  3. 빌드 캐시를 활용하도록 명령어 순서를 최적화하세요.
  4. .dockerignore로 불필요한 파일을 제외하세요.
  5. 그리고 최종 병기, 멀티스테이지 빌드를 사용해 빌드 환경과 실행 환경을 완벽히 분리하세요!

이 원칙들을 여러분의 Dockerfile에 적용해 보세요. 아마 놀랍도록 가벼워진 이미지와 빨라진 빌드 속도를 경험하게 되실 겁니다. Happy Dockering! 🐳


태그: Docker, Dockerfile, Multi stage build, 도커, 도커파일, 멀티스테이지 빌드, 이미지 최적화, DevOps, 컨테이너, CI, CD