Docker 파이썬 이미지가 1GB일 때 — slim·alpine·멀티스테이지 비교

Docker 파이썬 이미지가

Docker 파이썬 이미지가 1GB일 때 가장 먼저 의심해야 할 건 베이스 이미지입니다. python:3.12처럼 태그를 명시하지 않거나 기본 태그를 그대로 쓰면 Debian 전체 패키지와 빌드 도구가 딸려오면서 이미지가 1GB 안팎까지 불어납니다. slim 태그로 바꾸면 130~150MB대로, alpine 태그로 바꾸면 50MB 안팎까지 줄어들지만 둘 다 공짜로 얻는 이득은 아닙니다. 이 글에서는 세 방식의 실제 차이와, alpine으로 넘어갔을 때 흔히 터지는 빌드 실패 원인까지 함께 정리합니다.

Docker 파이썬 이미지가 1GB에 육박하는 이유

Docker Hub의 python:3.12 기본 태그는 Debian bookworm을 베이스로 사용합니다. 이 베이스에는 gcc, make, libssl-dev 같은 컴파일 도구와 개발 헤더가 기본으로 포함돼 있어서, 당장 pip install 한 번만 해도 바로 빌드가 되도록 설계돼 있습니다.

편리한 대신 대가가 따릅니다. 실제 애플리케이션 실행에는 전혀 쓰이지 않는 컴파일러, man 페이지, 로케일 파일까지 이미지 레이어에 그대로 남아서 최종 이미지 용량을 키웁니다. 다음 명령으로 직접 확인할 수 있습니다.

docker pull python:3.12
docker pull python:3.12-slim
docker pull python:3.12-alpine
docker images | grep python

버전과 빌드 시점에 따라 수치는 조금씩 달라지지만, 기본 태그가 slim이나 alpine 대비 몇 배 큰 걸 바로 확인할 수 있습니다. 배포 환경에서 이미지 pull 시간과 레지스트리 용량이 문제가 된다면 이 차이가 체감됩니다.

slim과 alpine, 실제로 얼마나 차이 날까요?

두 태그 모두 “작다”는 공통점이 있지만 줄이는 방식이 다릅니다. slim은 Debian 기반을 유지하면서 불필요한 패키지만 빼낸 버전이고, alpine은 아예 다른 리눅스 배포판인 Alpine Linux를 베이스로 씁니다. 이 차이가 호환성 문제의 근본 원인입니다.

Docker 컨테이너 고래 로고와 파란 배경

slim은 glibc를 그대로 쓰기 때문에 PyPI에 올라온 manylinux 형식의 미리 컴파일된 휠(wheel) 파일을 거의 그대로 설치할 수 있습니다. 반면 alpine은 C 표준 라이브러리로 musl을 쓰는데, PyPI의 바이너리 휠 대부분은 glibc 기준으로 빌드돼 있어서 alpine에서는 설치할 수 없는 경우가 많습니다.

이미지 태그 베이스 용량(대략) libc 바이너리 휠 호환
python:3.12 Debian bookworm 1GB 안팎 glibc 대부분 호환
python:3.12-slim Debian bookworm(경량) 130~150MB glibc 대부분 호환
python:3.12-alpine Alpine Linux 50MB 안팎 musl 일부 패키지 소스 컴파일 필요

표의 수치는 버전과 빌드 시점에 따라 변동될 수 있으므로, 실제 배포 전에는 반드시 docker images로 직접 확인하는 걸 권장합니다.

멀티스테이지 빌드로 줄여보세요

slim이나 alpine만으로는 해결 안 되는 상황이 있습니다. 애플리케이션에 numpy, cryptography처럼 C 확장이 필요한 패키지가 섞여 있으면, 설치 과정에서만 gcc와 헤더 파일이 필요하고 실행 시점에는 필요 없습니다. 이럴 때 멀티스테이지 빌드로 빌드 전용 단계와 실행 전용 단계를 분리하면 최종 이미지에 컴파일러가 남지 않습니다.

# 1단계: 빌드 전용 (컴파일 도구 포함)
FROM python:3.12 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# 2단계: 실행 전용 (slim만 사용)
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app.py"]

builder 단계에서 설치한 패키지만 /root/.local 경로로 복사해오기 때문에, 최종 이미지에는 gcc나 make 같은 빌드 도구가 전혀 남지 않습니다. 결과적으로 slim 단독 사용보다 더 작은 최종 이미지를 얻으면서도, 컴파일이 필요한 패키지는 문제없이 설치할 수 있습니다.

용량 차이보다 빌드 안정성이 더 큰 변수입니다

이미지 용량만 보고 무조건 alpine을 고르면 나중에 더 큰 비용을 치르게 됩니다. CI 파이프라인에서 멀쩡하던 빌드가 alpine으로 바꾼 뒤 갑자기 몇 분씩 더 걸리거나, 아예 실패하는 사례가 흔합니다.

원인은 대부분 바이너리 휠 호환성입니다. pip가 musl 기준의 미리 빌드된 휠을 찾지 못하면 소스 코드부터 직접 컴파일을 시도하는데, 이 과정에서 gcc, musl-dev, linux-headers 같은 패키지를 수동으로 설치해야 합니다.

FROM python:3.12-alpine
RUN apk add --no-cache gcc musl-dev linux-headers
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]

이렇게 빌드 도구를 추가하면 alpine의 장점인 “압도적으로 작은 용량”이 상당 부분 희석됩니다. 멀티스테이지 빌드를 alpine에도 똑같이 적용하지 않으면, 결과적으로 slim보다 별로 작지 않은 이미지가 나오는 경우도 있습니다.

alpine으로 바꿨는데 빌드가 깨지는 이유는 뭘까요?

가장 흔한 질문입니다. 답은 musl과 glibc의 ABI(애플리케이션 바이너리 인터페이스) 차이에 있습니다. PyPI의 패키지 배포자들은 보통 glibc 기준의 manylinux 태그로 휠을 올리는데, musl 환경을 위한 별도 표준인 musllinux 태그로 휠을 함께 올리는 패키지는 아직 제한적입니다. 이 표준은 musllinux 규격으로 정의돼 있는데, numpy나 pandas처럼 널리 쓰이는 패키지도 musllinux 휠을 제공하지 않으면 alpine에서 소스 빌드가 강제됩니다.

소스 빌드 자체가 실패하는 경우도 있습니다. 일부 C 확장 코드는 glibc의 특정 함수를 전제로 작성돼 있어서, musl 환경에서는 컴파일 에러나 런타임 세그폴트가 발생하기도 합니다. 이런 패키지를 쓰는 프로젝트라면 alpine보다 slim 기반 멀티스테이지 빌드가 더 안전한 선택입니다.

프로젝트 성격에 따라 이렇게 골라보세요

Docker 파이썬 이미지가 1GB일 때 고민할 선택지를 정리하면 이렇습니다. requirements.txt에 순수 파이썬 패키지만 있거나 가벼운 웹 서비스라면 slim 단독으로도 충분히 작고 안전합니다. numpy, pandas, cryptography처럼 C 확장이 섞여 있다면 slim 베이스의 멀티스테이지 빌드가 용량과 호환성 두 마리 토끼를 동시에 잡는 방법입니다.

용량을 극단적으로 줄여야 하는 임베디드성 환경이나 패키지 의존성이 아주 단순한 경우에만 alpine을 고려하되, 반드시 CI에서 실제 빌드 테스트를 거친 뒤 도입하는 걸 권장합니다. 세 방식 중 정답은 하나가 아니라, 프로젝트의 의존성 목록을 먼저 들여다보는 쪽이 순서상 맞습니다.

PyTorch Docker 이미지 6.8GB에서 800MB로 줄이기 (멀티스테이지 + CPU 전용 휠)

Leave a Comment