[도커 30강] 18강. 멀티스테이지 빌드로 이미지 경량화
도커 이미지를 만들 때 가장 흔한 실수 중 하나는 “빌드는 잘 되는데 이미지가 너무 크다”는 상태를 방치하는 것입니다. 처음에는 몇 백 MB 차이가 별것 아닌 것처럼 보이지만, 서비스가 늘고 배포 빈도가 올라가면 이 차이가 빌드 시간, 전송 시간, 롤백 속도, 보안 스캔 시간까지 전부 악화시킵니다. 특히 CI/CD 파이프라인에서 매번 큰 이미지를 푸시/풀하면 개발팀 전체의 대기 시간이 누적되어 생산성이 눈에 띄게 떨어집니다.
이번 강의에서는 멀티스테이지 빌드를 단순 문법이 아니라 운영 효율을 만드는 설계 도구로 다룹니다. 핵심은 “빌드에 필요한 것”과 “실행에 필요한 것”을 분리하는 것입니다. 컴파일러, 패키지 관리자, 테스트 도구는 빌드 스테이지에만 두고, 런타임 스테이지에는 실행에 필요한 최소 산출물만 남깁니다. 이 구조를 익히면 같은 애플리케이션도 훨씬 작고 빠르고 안전한 이미지로 만들 수 있습니다.
핵심 개념
- 멀티스테이지 빌드는 하나의 Dockerfile 안에 여러
FROM스테이지를 두고, 필요한 산출물만 다음 스테이지로 복사하는 방식입니다. - 첫 스테이지(빌더)는 “무겁지만 작업하기 좋은 환경”, 마지막 스테이지(런타임)는 “가볍고 안전한 실행 환경”으로 분리합니다.
COPY --from=<stage>를 사용하면 빌더 스테이지 전체가 아니라, 원하는 파일/디렉터리만 정확히 가져올 수 있습니다.- 이미지 용량 감소는 단순 저장공간 절약이 아니라 배포 속도, 네트워크 비용, 장애 대응 시간(롤백 포함)을 줄이는 실무 효과로 이어집니다.
- 런타임 이미지에서 불필요한 툴(컴파일러, curl, git, 빌드 캐시)을 제거하면 공격면도 함께 줄어 보안상 유리합니다.
기본 사용
예제 1) 단일 스테이지와 멀티스테이지 결과 비교
먼저 “왜 멀티스테이지가 필요한지”를 눈으로 확인해보겠습니다. 같은 앱을 단일 스테이지와 멀티스테이지로 각각 빌드한 뒤 용량을 비교합니다.
mkdir -p ~/docker100/lesson18/app
cd ~/docker100/lesson18
cat > app/main.py <<'PY'
print("hello from lesson18")
PY
cat > Dockerfile.single <<'DOCKER'
FROM python:3.12
WORKDIR /app
COPY app/main.py /app/main.py
RUN apt-get update && apt-get install -y build-essential curl && rm -rf /var/lib/apt/lists/*
CMD ["python", "/app/main.py"]
DOCKER
cat > Dockerfile.multi <<'DOCKER'
FROM python:3.12 AS builder
WORKDIR /build
COPY app/main.py /build/main.py
RUN python -m compileall /build
FROM python:3.12-slim AS runtime
WORKDIR /app
COPY --from=builder /build/main.py /app/main.py
CMD ["python", "/app/main.py"]
DOCKER
docker build -f Dockerfile.single -t lesson18:single .
docker build -f Dockerfile.multi -t lesson18:multi .
docker images | grep lesson18
설명:
Dockerfile.single은 편하지만 빌드/실행 도구가 한 이미지에 섞여 비대해지기 쉽습니다.Dockerfile.multi는 builder에서 작업 후 runtime에 최소 파일만 옮겨 이미지가 작아집니다.- 실무에서는 언어별 의존성 구조가 더 복잡하므로 체감 차이가 더 크게 납니다.
예제 2) Python 의존성 설치를 builder로 보내고 런타임 최소화
Python 프로젝트에서 자주 쓰는 패턴은 wheel 생성/설치를 builder에 몰아두고, 런타임에는 앱 실행 최소 패키지만 남기는 방식입니다.
cd ~/docker100/lesson18
cat > requirements.txt <<'REQ'
flask==3.0.3
gunicorn==22.0.0
REQ
cat > Dockerfile <<'DOCKER'
FROM python:3.12 AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --upgrade pip && pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
FROM python:3.12-slim AS runtime
WORKDIR /app
COPY --from=builder /wheels /wheels
COPY requirements.txt .
RUN pip install --no-cache-dir --no-index --find-links=/wheels -r requirements.txt && rm -rf /wheels
COPY app/ /app/
EXPOSE 8000
CMD ["gunicorn", "-b", "0.0.0.0:8000", "main:app"]
DOCKER
docker build -t lesson18:python-multi .
docker run --rm -p 8000:8000 lesson18:python-multi
설명:
- builder에서 wheel을 만들면 런타임에서 네트워크 의존 없이 설치할 수 있어 재현성이 좋아집니다.
python:3.12-slim을 런타임 베이스로 쓰면 기본 용량을 줄일 수 있습니다.- 설치 후
/wheels를 지워 불필요 파일이 최종 이미지에 남지 않게 합니다.
예제 3) 빌드 타깃(stage target)으로 디버그/운영 이미지 분리
멀티스테이지의 장점은 단순 경량화뿐 아니라, 디버그용 스테이지와 운영용 스테이지를 함께 관리할 수 있다는 점입니다.
cd ~/docker100/lesson18
cat > Dockerfile.target <<'DOCKER'
FROM python:3.12 AS builder
WORKDIR /src
COPY app/ /src/
FROM builder AS debug
RUN pip install debugpy
CMD ["python", "-m", "debugpy", "--listen", "0.0.0.0:5678", "/src/main.py"]
FROM python:3.12-slim AS prod
WORKDIR /app
COPY --from=builder /src /app
CMD ["python", "/app/main.py"]
DOCKER
# 디버그 이미지
docker build -f Dockerfile.target --target debug -t lesson18:debug .
# 운영 이미지
docker build -f Dockerfile.target --target prod -t lesson18:prod .
docker images | grep 'lesson18:'
설명:
- 한 Dockerfile에서
--target으로 목적별 이미지를 분리하면 유지보수가 쉬워집니다. - debug 툴은 debug 스테이지에만 있고 prod에는 포함되지 않아 운영 보안에 유리합니다.
- 팀 규칙으로 “배포는 반드시 prod 타깃”을 고정하면 실수 확률을 크게 줄일 수 있습니다.
예제 4) 이미지 내부 검증과 레이어 확인
이미지가 정말 가벼워졌는지, 불필요 파일이 남아 있는지 확인하는 습관이 중요합니다.
# 이미지 레이어/크기 확인
docker history lesson18:python-multi
# 컨테이너 진입 후 설치 파일 확인
docker run --rm -it lesson18:python-multi sh -lc 'ls -al /app && python --version'
# 취약점 스캔 도구가 있다면 스캔도 병행
# docker scout quickview lesson18:python-multi
설명:
docker history로 어떤 레이어가 용량을 크게 차지하는지 찾을 수 있습니다.- 최종 런타임에 빌드 전용 파일이 남아 있지 않은지 직접 확인해야 합니다.
- 경량화와 함께 보안 스캔을 병행하면 운영 품질이 안정됩니다.
자주 하는 실수
실수 1) 멀티스테이지를 썼지만 COPY . .로 불필요 파일까지 전부 복사
- 원인: 빠르게 작성하려고 전체 복사 습관을 유지.
- 문제: 테스트 데이터,
.git, 로컬 캐시까지 이미지에 들어가 용량/보안 모두 악화. - 해결: 필요한 파일만 명시적으로
COPY하고.dockerignore를 반드시 구성합니다.
실수 2) 런타임 스테이지에서 다시 apt/pip 설치를 크게 수행
- 원인: builder에서 분리해놓고 runtime에서 다시 무거운 작업 수행.
- 문제: 경량화 이점이 거의 사라지고 빌드 시간도 다시 길어짐.
- 해결: builder에서 산출물(wheel, binary, static assets)을 만들고 runtime은 복사+최소 설치만 수행합니다.
실수 3) stage 이름 없이 숫자 인덱스만 사용
- 원인:
--from=0처럼 짧게 쓰는 편의성 추구. - 문제: Dockerfile 수정 시 스테이지 순서가 바뀌면 잘못된 산출물을 복사하는 사고 발생.
- 해결:
AS builder,AS runtime처럼 의미 있는 이름을 붙이고--from=builder를 사용합니다.
실수 4) 디버그 도구를 운영 이미지에 남김
- 원인: 문제 대응 편의 때문에
curl,vim,debugpy를 runtime에 유지. - 문제: 공격면 증가, 이미지 크기 증가, 규정 위반 가능성.
- 해결: 디버그용 타깃 이미지를 별도 유지하고 운영 배포 파이프라인은 prod 타깃만 허용합니다.
실무 패턴
실무에서 멀티스테이지를 안정적으로 운영하려면 다음 패턴을 권장합니다.
-
패턴 1: Builder 표준화
언어별로 검증된 builder 베이스를 고정합니다. 예: Python은 wheel 기반, Node는 build artifact(dist) 기반, Go는 static binary 기반. -
패턴 2: Runtime 최소화
가능하면 slim/alpine/distroless 계열 런타임을 검토합니다. 단, 디버깅 난이도와 libc 호환 이슈를 고려해 팀 표준을 정합니다. -
패턴 3: 빌드 캐시 전략
의존성 파일(requirements.txt,package-lock.json)을 먼저 복사하고 설치한 뒤 앱 코드를 복사해 캐시 재사용률을 높입니다. -
패턴 4: 배포 게이트
CI에서 이미지 크기 상한, 필수 파일 존재, 금지 패키지 포함 여부를 체크해 기준을 넘으면 배포를 막습니다. -
패턴 5: 태그와 추적성
myapp:1.4.2,myapp:git-<sha>처럼 버전 추적 가능한 태그를 사용하고, 롤백 시 동일 이미지를 빠르게 재사용합니다.
결국 멀티스테이지의 목적은 “Dockerfile을 예쁘게 짜기”가 아니라, 팀이 빠르게 빌드하고 안전하게 배포하며 장애 시 신속히 복구할 수 있는 운영 체계를 만드는 데 있습니다. 이미지가 작아질수록 CI/CD, 레지스트리 비용, 배포 대기 시간이 동시에 개선되므로 투자 대비 효과가 큰 최적화입니다.
오늘의 결론
한 줄 요약: 멀티스테이지 빌드는 빌드 환경과 실행 환경을 분리해, 더 작고 빠르고 안전한 이미지를 만드는 실무 필수 패턴이다.
연습문제
- 현재 사용 중인 Dockerfile을 멀티스테이지로 바꾸고, 변경 전/후 이미지 크기와 빌드 시간을 비교해보세요.
- builder 스테이지에서만 필요한 도구(컴파일러, git, debug 패키지)를 분리한 뒤 런타임 이미지에 남아 있지 않은지 검증해보세요.
--target debug와--target prod를 분리해 만든 다음, CI에서 prod 타깃만 배포되도록 파이프라인 조건을 추가해보세요.
이전 강의 정답
17강 연습문제 해설:
compose.dev.yaml에만 둘 요소는 보통 바인드 마운트(코드 즉시 반영), 디버그 포트 노출, 핫리로드 커맨드입니다. 운영에서 이를 제외하는 이유는 재현성 저하, 공격면 증가, 성능/안정성 변동을 막기 위해서입니다.compose.prod.yaml의read_only,restart, 자원 제한은 운영 안전장치입니다.docker compose config로 병합 결과를 보면 베이스 대비 어떤 값이 최종 적용되는지 명확히 확인할 수 있고, 리뷰 시 실수를 줄일 수 있습니다.Makefile에dev-up,prod-up,config-prod를 표준화하면 팀원이 같은 진입점으로 실행하게 되어 환경 차이 이슈가 크게 줄어듭니다. PR 체크리스트에 “prod 조합으로 config 검증 완료” 항목을 넣으면 잘못된 파일 조합 배포를 예방할 수 있습니다.
실습 환경/재현 정보
- OS: macOS 15+ (Apple Silicon) 또는 Ubuntu 22.04+
- Docker: Engine/Desktop 25.x 이상
- Compose: v2.x
- 예제 파일:
Dockerfile.single,Dockerfile.multi,Dockerfile,Dockerfile.target,requirements.txt - 실행 순서:
- 단일/멀티 Dockerfile 작성
- 각각 빌드 후 이미지 크기 비교
- 멀티스테이지 런타임 컨테이너 실행 확인
docker history로 레이어 점검- debug/prod 타깃 분리 전략 적용
- 재현 체크:
- 멀티스테이지 이미지가 단일 스테이지보다 작아졌는가?
- 런타임 이미지에 빌드 도구가 남아 있지 않은가?
- prod 타깃으로 실행했을 때 서비스가 정상 기동하는가?
- CI에서 prod 타깃만 배포되도록 제어 가능한가?