Python에서 Alpine 이미지를 사용하면 안 되는 이유

Python에서 Alpine 이미지를 사용하면 안 되는 이유

Docker Alpine Image란?

Docker를 사용할 때, 많은 경우에 Alpine 이미지를 사용하게 됩니다.

 

Docker에서 Linux 이미지를 사용할 때는 여러 가지 옵션이 존재합니다.

  GNU Linux 배포판 Slim Alpine
특징 아무 태그를 붙이지 않으면 기본적으로 사용 최소한의 패키지로만 구성된 경량 이미지 컨테이너 배포용 경량 이미지
용량 매우 큼 작음 작음
이미지 빌드 속도 매우 느림 빠름 빠름
예시 FROM ubuntu:20.04 FROM python:3.9.2-slim FROM python:3.9.2-alpine

보통 Alpine 이미지가 컨테이너 전용으로 개발된 경량 이미지여서, Alpine을 많이 사용합니다.

실제로 이미지 크기를 비교하면, 정말 많이 차이가 납니다.

$ docker image ls ubuntu && docker image ls alpine
REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
ubuntu       latest    6a47e077731f   4 weeks ago   69.2MB
REPOSITORY   TAG       IMAGE ID       CREATED       SIZE
alpine       latest    f6648c04cd6c   6 weeks ago   7.66MB

그런데 Python에서는 Alpine 이미지를 사용하면 안 된다고 합니다.

Python에서 Alpine 이미지를 사용하면 안 되는 이유

Itamar Turner Trauring은 slim과 alpine을 base image로 하여 Python Image를 빌드한 실험에서, 다음과 같은 결과를 내놓았습니다 [1].

Base image Time to build Image size Research required
python:3.8-slim 30 seconds 363MB No
python:3.8-alpine 1557 seconds 851MB Yes

Ryan Pepper가 행한, bullseye와 alpine을 비교한 실험에서는 다음과 같은 결과가 나왔습니다 [2].

Base Image Tag Build time Base Image Size (MB) Image size (MB)
3.10.8-bullseye 00:00:26 921.1 1118.7
3.10.8-alpine3.17 00:23:40 50.0 194.5

두 실험 모두, alpine이 다른 base image에 비해서 build time이 크게 증가한 모습을 볼 수 있습니다.

 

다른 이미지에서는 30초 이내로 빌드가 완료되는데, Alpine 이미지 사용 시 20분이 넘게 걸리는 모습을 볼 수 있습니다.

 

특히 slim 이미지와 비교하면, Image Size 크기 면에서도 Alpine은 약한 모습을 보입니다.

 

확실히, Alpine 이미지를 사용하면 안 되겠다는 생각이 듭니다.

원인 분석

결론부터 말하자면, Python은 Python으로만 이루어져 있지 않다는 언어적 특성 때문입니다.

 

Python은 내부 구현체로 C를 사용하고 있습니다. numpy, keras, pandas, 그 외 많은 Python 라이브러리들이 내부적으로 C로 구현된 체, 인터페이스만 Python으로 제공하고 있습니다.

 

그래서 Python을 돌리기 위해서는 glibc 라는 C언어 컴파일을 위한 시스템 라이브러리를 갖고 있어야 합니다.

 

문제는 Alpine Linux는, glibc를 사용하지 않습니다 [3]. 대신 MUSL이라는 별도의 C 라이브러리를 사용합니다. MUSL은 가볍다는 장점을 갖고 있지만, 모든 바이너리에 static하게 연결되어야 하는 특성을 갖고 있습니다.

 

그래서 MUSL에 연결하기 위해서, Python 내부의 C 구현체를 컴파일할 때 추가적인 작업을 하게 됩니다.

 

그래서 Alpine 이미지를 사용할 때, MUSL의 존재 때문에 빌드 시간이 늘어나고, 전체적인 이미지 용량도 커지게 됩니다.

 

이 외에도 Alpine 이미지가 MUSL을 이용하기 때문에, 여러 가지 빌드 과정에서의 충돌이 보고되는 것 같습니다. Rogan Lynch는 MUSL이 Alpine의 가장 큰 약점이라 평하기도 했습니다 [4].

그럼 무슨 이미지를 사용해야 하나? - 이미지 별 성능 비교

Slim 이미지는 기본 C 라이브러리로 glibc를 사용하기에 이를 이용해도 좋습니다.

 

혹은 Debian Buster 키워드가 붙어있는 이미지를 사용하면 더 좋다고 합니다 [5].

 

그래서 직접 기본 python 이미지, debian-buster, slim, bullseye, alpine 이미지를 빌드하는 시간을 비교해봤습니다.

실험 세팅

사용한 Dockerfile은 다음과 같습니다.

FROM python:3.9

RUN apt-get update && apt-get install -y \
    gcc \
    libfreetype6-dev \
    libpng-dev \
    libopenblas-dev \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .

RUN pip install --no-cache-dir --upgrade pip && pip install --no-cache-dir -r requirements.txt

맨 위에 FROM python:3.9 (Debian)에서 이미지 버전만 바꾸어 사용했습니다.

예를 들어서 slim이면 FROM python:3.9-slim 이런 식입니다.

 

참고로 기본 Python 이미지가 debian 입니다. debian-buster는 FROM python:3.9-buster로 시작합니다.

 

그리고 Alpine 버전의 경우 기본 apt-get을 사용하지 않고, apk를 사용합니다. 따라서 Alpine만 살짝 다릅니다.

FROM python:3.9-alpine

RUN apk --update add gcc \
    build-base \
    freetype-dev \
    libpng-dev \
    openblas-dev

COPY requirements.txt .

RUN pip install --no-cache-dir --upgrade pip && pip install --no-cache-dir -r requirements.txt

추가로 alpine의 경우 python 패키지를 사용하기 위해서는, 두 번째 RUN apk add gcc 로 시작되는, 추가적인 라이브러리를 설치해야 합니다.

 

slim, debian 등은 두 번째 과정이 필요하지 않습니다. 그러나 테스트의 공정성을 위해서, 나머지 Dockerfile 에도 필요한 requirements들을 설치해주었습니다. 실제로 이용할 때에는 Alpine이 아닌 경우 이 과정을 생략해도 좋습니다.

 

마지막으로, requirements.txt는 다음과 같습니다. pip install keras pandas matplotlib numpy 를 한 것과 같습니다.

contourpy==1.1.1
cycler==0.11.0
fonttools==4.42.1
keras==2.14.0
kiwisolver==1.4.5
matplotlib==3.8.0
numpy==1.26.0
packaging==23.1
pandas==2.1.0
Pillow==10.0.1
pyparsing==3.1.1
python-dateutil==2.8.2
pytz==2023.3.post1
six==1.16.0
tzdata==2023.3

결과

종류 빌드 시간 (s) 이미지 크기 (MB)
default 22 1310
slim 29 666
debian-buster 42 1110
bullseye 45 1160
alpine 461 626

이미지 별 성능 비교 결과 차트
이미지 별 성능 비교 결과
각 이미지의 크기
각 이미지의 크기
좌: 기본 / 중: slim / 우: debian-buster
좌: 기본 / 중: slim / 우: debian-buster
좌: bullseye / 우: alpine

결과 해석

아니 debian buster가 가장 좋다면서요…

도커, 컨테이너 빌드업 p.158
도커, 컨테이너 빌드업 p.158

속았습니다. slim이 가장 좋습니다.

 

일단 alpine은 container 용으로 나온 버전답게 차지하고 있는 이미지 용량이 가장 작습니다(626MB).

그러나 이미지 빌드 시간이 7분 41초로 다른 base image에 비해서 압도적으로 오랜 시간이 걸립니다.

 

가장 시간이 짧게 걸린 것은, default 버전이었습니다(22초). 그러나 용량이 1.31GB로 꽤 큽니다.

 

slim은 29초라는 준수한 시간, 그리고 666MB라는 작은 용량으로 매우 좋은 모습을 보여주었습니다.

 

결론적으로 직접 실험해본 결과 slim을 선택하는 것이 가장 합리적으로 보입니다.

주의점

사실, 위 실험 결과는 다소 인위적인 측면이 있습니다.

 

일부러 C 구현체로 되어 있는 matplotlib, keras, pandas와 같은 라이브러리를 설치하고 있기 때문입니다.

때문에 7분씩이나 걸리는 빌드 시간은, 일부러 Alpine에게 불리한 조건을 주어서 과장시킨 면이 있습니다.

 

C 구현체의 양이 적은, 예컨대 django와 같은 경우, 이미지 빌드 시간은 매우 짧아집니다.

FROM python:3.9-alpine

WORKDIR /app

COPY requirements.txt .

RUN pip install --upgrade pip && pip install -r requirements.txt

COPY . .

CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
asgiref==3.7.2
Django==4.2.5
sqlparse==0.4.4

기본 django 어플리케이션을 빌드하는데 1.22초가 걸립니다.

 

이처럼 기존에 Alpine 이미지를 사용하면서 문제가 없었으면, 굳이 Alpine을 탈피할 이유까지는 없어 보입니다. 문제점을 애초에 느끼지 못했을 수도 있습니다. 결과 해석 시에 주의합시다.

 

Beyond Database는, django 개발 시에 slim-buster를 이용하는 것을 가장 추천합니다 [6]. 저도 slim-buster를 이용해봐야겠습니다.

 

저의 경우에는 최근에 새롭게 django 프로젝트를 준비하고 있던 터라, 관련된 정보를 정리해보았습니다. 이 글이 도커 이미지 선택에 있어서 약간의 도움이 되길 바랍니다.

 

새 프로젝트를 시작하는 경우라면 Alpine은 그만 사용합시다.

참고 자료

  1. Using Alpine can make Python Docker builds 50× slower, Itamar Turner-Trauring, 2020
  2. Why using Alpine Docker images and Python is probably bad for your project (right now), Ryan Pepper, 2022
  3. 알파인 리눅스(Alpine Linux), lesstif
  4. musl-libc - Alpine's Greatest Weakness, Rogan Lynch, 2020
  5. 이현룡. 2021. 도커, 컨테이너 빌드업!. 파주: 제이펍. p: 158
  6. Django & Postgres with Docker Best Practices, Beyond Database, 2021