한 주니어 개발자가 Python Docker 이미지 크기를 1.2GB에서 85MB로 줄였고, 이로 인해 CI 파이프라인 시간이 11분에서 90초로 단축되었습니다. 이미지가 작을수록 다운로드 속도가 빨라지고, 저장 비용이 절감되며, 보안 취약점 노출도 줄어듭니다.

이미지 크기가 중요한 이유

컨테이너를 풀(pull)할 때마다 레지스트리는 이미지 전체를 스트리밍합니다. 85MB 레이어는 몇 초 만에 내려받을 수 있지만, 1.2GB 레이어는 일반적인 네트워크 환경에서 몇 분이 걸릴 수 있습니다. 빌드 에이전트 또한 전체 이미지를 업로드하고 캐싱해야 하므로 CI 시간과 클라우드 스토리지 비용이 증가합니다. 추가되는 패키지가 많을수록 잠재적인 취약점이 될 수 있으므로, 베이스 이미지를 다이어트하면 공격 표면(attack surface)을 줄일 수 있습니다.

용량이 커지는 원인

  • gcc와 같은 빌드 도구가 앱을 실행하는 동일한 스테이지에 설치되어 있으면 최종 이미지에 그대로 남습니다.
  • 패키지 매니저 캐시(예: apt 또는 pip 캐시)가 디스크에 남아 기본적으로 정리되지 않습니다.
  • 모든 RUN 명령은 새로운 읽기 전용(read-only) 레이어를 생성하며, 레이어 간에 중복된 파일이 쌓이게 됩니다.
  • ubuntu:latest와 같은 대형 베이스 이미지는 최소한의 Python 런타임에 필요한 것보다 훨씬 많은 전체 OS를 포함하고 있습니다.

3단계 축소 과정

단계 베이스 이미지 빌드 방식 결과 크기
1 표준 Python (전체) 단일 스테이지, 모든 도구 포함 1.18GB
2 python:slim 멀티 스테이지: gcc를 포함한 builder 스테이지 사용, 최종 스테이지에서는 컴파일된 패키지만 복사 210MB
3 python:alpine 매우 가벼운 Alpine Linux 기반의 멀티 스테이지 85MB

1단계 – 기준점(Baseline)

기본 Python 이미지로 시작했을 때, 개발자는 1.18GB의 결과물을 확인했습니다. 이 이미지에는 전체 Debian 스택, 개발용 헤더, 그리고 pip 캐시가 포함되어 있었습니다.

2단계 – builder를 활용한 slim 이미지

python:slim으로 전환하여 OS 점유율을 줄였지만, 빌드 도구는 여전히 남아 있었습니다. builder 스테이지를 추가하여 gcc, make 및 기타 컴파일 시 필요한 의존성을 설치, 컴파일한 후 제거할 수 있었습니다. 최종 스테이지에서는 COPY --from=builder를 사용하여 컴파일된 wheel 파일과 런타임 파일만 가져왔고, 그 결과 크기가 210MB로 줄어들었습니다.

3단계 – Alpine의 승리

Alpine Linux는 musl libc와 busybox를 기반으로 구축되었습니다. Alpine에서 멀티 스테이지 패턴을 반복 적용한 결과, 기존 대비 93%가 감소한 85MB의 이미지를 만들 수 있었습니다. 개발자에 따르면 Kubernetes가 이미지를 몇 초 만에 풀(pull)했으며, CI 작업도 90초 만에 완료되었습니다.

실질적인 효과

  • 스토리지 비용 절감 – 레지스트리 저장 공간이 줄어듭니다.
  • 보안 강화 – 패키지 수가 적을수록 추적해야 할 CVE(취약점)가 줄어듭니다. Alpine 이미지는 Python 런타임과 애플리케이션 코드만 포함합니다.
  • CI 속도 향상 – 파이프라인 실행 시간이 11분에서 90초로 단축되었습니다.

Dockerfile 다이어트를 위한 실무 팁

  • :latest 태그 사용을 피하고, 필요에 맞는 :slim 또는 :alpine 변형 버전을 선택하세요.
  • 멀티 스테이지 빌드를 사용하세요. 컴파일을 위한 전용 builder 스테이지와 실제 필요한 아티팩트만 받는 runtime 스테이지를 분리합니다.
  • COPY --from=builder /path/to/installed /path/in/final를 사용하여 선택적으로 복사하세요.
  • 의존성 설치가 소스 코드 복사보다 먼저 실행되도록 명령 순서를 조정하세요. 이렇게 하면 레이어 캐싱을 극대화할 수 있습니다.
  • rm -rf /var/lib/apt/lists/* ~/.cache/pip와 같이 캐시를 명시적으로 삭제하세요.

Alpine을 채택할 경우, 앱에서 DNS 확인 문제가 발생하면 nss 패키지를 추가하세요. pip로 설치할 때는 로컬에 설치된 스크립트를 런타임에 찾을 수 있도록 PATH 앞에 /root/.local/bin을 추가하세요.

주의 사항

Alpine의 musl libc는 glibc용으로 컴파일된 바이너리 wheel과 충돌하여 런타임 오류를 일으킬 수 있습니다. 이 경우 Alpine builder 내부에서 wheel을 다시 빌드하거나 slim 베이스로 돌아가야 합니다. 추가적인 nss 패키지 설치는 DNS 신뢰성을 위한 작은 비용입니다.

향후 점검 사항

  • 기존 이미지를 스캔하여 멀티 스테이지 재작성이 필요한 대형 레이어가 있는지 확인하세요.
  • CI 로그의 업로드 및 다운로드 시간을 모니터링하여 이미지 비대화(bloat)를 감지하세요.
  • 선택한 베이스 배포판의 취약점 보고서를 지속적으로 확인하세요.

교훈은 명확합니다. 규율 있는 Dockerfile, 가벼운 베이스 이미지, 그리고 builder 스테이지를 활용하면 Python 이미지 크기를 10배 이상 줄일 수 있으며, 기능 손실 없이 실질적인 속도와 비용 이점을 얻을 수 있습니다.