Python 애플리케이션을 위한 효율적인 Dockerfile 작성법

How to Write Efficient Dockerfiles for Your Python Applications

Docker는 Python 애플리케이션 배포를 단순화했습니다. 하지만 최적화되지 않은 컨테이너는 이미지 크기 증가, 빌드 속도 저하, 보안 문제를 초래할 수 있습니다.

이 글에서는 경험 있는 Python 및 Docker 개발자가 컨테이너화 워크플로우를 간소화하기 위해 구현할 수 있는 실용적인 기술에 초점을 맞추겠습니다.

기본 사항은 건너뛰고 빌드 시간과 이미지 크기에 실질적인 차이를 만들어낼 기술에 집중해 보겠습니다.

1. 필요에 맞는 특정 기본 이미지 사용하기

특정 요구 사항에 따라 기본 이미지를 신중하게 선택하세요.

표준 python 이미지에는 프로덕션 환경에서 필요하지 않은 많은 개발 도구가 포함되어 있습니다. slim 변형은 크기와 호환성 사이에서 좋은 균형을 제공하며, alpine은 매우 작지만 C 확장이 있는 패키지를 위해 추가 작업이 필요할 수 있습니다.

대부분의 애플리케이션용

FROM python:3.11-slim

순수 Python 애플리케이션용

FROM python:3.11-slim-bullseye

가능한 가장 작은 이미지용(호환성 문제 가능성 있음)

FROM python:3.11-alpine

단순히 습관적으로 기본 이미지를 사용하지 말고, 애플리케이션 요구에 가장 적합한 변형을 평가하세요. 기본 이미지 선택은 다른 어떤 최적화보다 최종 이미지 크기에 더 큰 영향을 미칠 수 있습니다.

2. 보안을 위한 비루트 사용자 설정

컨테이너를 루트 사용자로 실행하지 마세요. 루트로 실행되는 컨테이너가 손상되면 공격자가 호스트 시스템에 접근할 가능성이 있습니다.

비특권 사용자를 생성하고 사용함으로써 이러한 위험을 줄일 수 있습니다. 이는 모든 프로덕션 컨테이너에서 표준이 되어야 하는 보안 모범 사례입니다.

비특권 사용자 생성

RUN addgroup --system appgroup && \

adduser --system --ingroup appgroup appuser && \

chown -R appuser:appgroup /app

해당 사용자로 전환

USER appuser

CMD ["python3", "app.py"]

애플리케이션이 특권 포트에 바인딩해야 하는 경우, 루트로 실행하는 대신 리버스 프록시를 사용하거나 호스트 포트 매핑을 조정하는 것을 고려하세요.

3. 캐시 효율성을 위한 명령어 순서 최적화

Docker 빌드 속도를 높이는 가장 효과적인 방법 중 하나는 레이어 캐싱 시스템을 활용하는 것입니다. Docker는 빌드의 각 레이어를 캐시하고, 변경되지 않은 경우 이러한 레이어를 재사용합니다. Dockerfile 명령을 전략적으로 배치하여 이 이점을 극대화할 수 있습니다.

예시 Dockerfile:

FROM python:3.11-slim

WORKDIR /app

의존성 파일을 먼저 복사하고 설치

COPY requirements.txt .

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

애플리케이션 코드를 마지막에 복사(가장 자주 변경됨)

COPY . .

CMD ["python3", "app.py"]

이 접근 방식은 의존성이 애플리케이션 코드와 별도의 레이어에 설치되도록 합니다.

코드는 의존성보다 훨씬 더 자주 변경되므로, Docker는 후속 빌드에서 캐시된 의존성 레이어를 재사용하여 빌드 시간을 크게 줄입니다.

3. 이미지 크기 최소화

특히 많은 인스턴스를 배포하거나 자주 업데이트하는 경우 컨테이너 이미지에서 모든 메가바이트가 중요합니다.

--no-cache-dir 플래그를 사용하면 pip가 다운로드한 패키지를 저장하지 않습니다. 정리 명령은 임시 파일과 패키지 목록을 제거합니다.

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .

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

# pip 캐시 제거

rm -rf /root/.cache/pip

불필요한 패키지 제거

RUN apt-get update && \

apt-get purge -y --auto-remove curl && \

apt-get clean && \

rm -rf /var/lib/apt/lists/*

COPY . .

CMD ["python3", "app.py"]

런타임에 필요하지 않은 패키지를 제거할 기회를 찾으세요. 작은 이미지는 저장 비용을 줄이고 공격 표면을 감소시킵니다.

4. 복잡한 의존성을 위한 다단계 빌드 구현

애플리케이션에 런타임에 필요하지 않은 컴파일 도구나 빌드 의존성이 필요한 경우, 다음과 같이 다단계 빌드를 사용할 수 있습니다.

빌드 단계

FROM python:3.11 AS builder

WORKDIR /build

COPY requirements.txt .

빌드 의존성 설치

RUN apt-get update && \

apt-get install -y --no-install-recommends gcc libpq-dev && \

pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt

최종 단계

FROM python:3.11-slim

WORKDIR /app

빌더에서 휠만 복사

COPY --from=builder /wheels /wheels

RUN pip install --no-cache-dir --no-index --find-links=/wheels /wheels/*

COPY . .

CMD ["python3", "app.py"]

이렇게 하면 첫 번째 단계에서 모든 빌드 의존성을 가진 복잡한 패키지를 빌드한 다음, 빌드된 휠만 최종 이미지로 복사할 수 있습니다. 결과적으로 경량화된 런타임 이미지를 얻을 수 있습니다.

5. 불필요한 Python 의존성 제거

의존성은 이미지 크기를 빠르게 증가시킬 수 있습니다. 이 접근 방식은 pipdeptree를 사용하여 직접적인 의존성을 식별한 다음 애플리케이션에서 직접 필요하지 않은 패키지를 제거합니다.

COPY requirements.txt .

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

...

# 불필요한 의존성 제거

pip install pipdeptree && \

pipdeptree --warn silence | grep -v '^\w' | cut -d ' ' -f 2 > /tmp/req_packages && \

pip freeze | grep -v -f /tmp/req_packages | xargs pip uninstall -y

개발 및 프로덕션을 위한 별도의 requirements 파일을 유지하여 테스트 프레임워크와

린터를 처음부터 프로덕션 이미지에 설치하지 않도록 고려하세요.

6. .dockerignore 파일 사용하기

빌드가 시작되기 전에 .dockerignore 파일을 생성하여 Docker 데몬으로 전송되는 내용을 최적화할 수 있습니다:

버전 관리

.git/

.gitignore

Python 아티팩트

pycache/

*.py[cod]

*$py.class

*.so

.pytest_cache/

.coverage

개발 환경

.env

.venv

빌드 아티팩트

dist/

build/

*.egg-info/

로컬 개발 파일

data/

logs/

*.log

이 파일은 Docker 빌드를 위한 .gitignore처럼 작동합니다. 이러한 파일을 제외하면 빌드 프로세스 속도가 향상되고(Docker 데몬에 전송되는 데이터가 줄어들어) 민감한 정보나 로컬 개발 아티팩트가 이미지로 유출될 가능성도 방지합니다.

7. BuildKit의 고급 기능 활용하기

Docker BuildKit은 탐색할 가치가 있는 강력한 기능을 제공합니다. 캐시 마운트 기능은 빌드 간에 지속적인 캐시를 생성하여 패키지 설치 속도를 높입니다.

로컬 캐시를 마운트하여 pip 속도 향상

RUN --mount=type=cache,target=/root/.cache/pip \

pip install -r requirements.txt

시크릿 마운트를 사용하면 빌드 중에 민감한 데이터를 사용하면서도 이미지 레이어에 포함되지 않도록 할 수 있습니다.

이미지에 포함시키지 않고 시크릿 마운트

RUN --mount=type=secret,id=dbpassword,dst=/run/secrets/dbpassword \

python -c 'import os; open("config.py", "w").write(f"PASSWORD = \"{open("/run/secrets/db_password").read().strip()}\"")'

DOCKER_BUILDKIT=1 환경 변수를 설정하거나 Docker 데몬 구성에서 BuildKit을 활성화할 수 있습니다.

결론

이러한 특정 기술을 구현함으로써 이미지 크기와 빌드 시간을 줄일 뿐만 아니라 더 유지 관리가 쉽고 안전한 컨테이너화된 Python 애플리케이션을 만들 수 있습니다.

컨테이너화는 반복적인 프로세스입니다. 애플리케이션 코드가 변경됨에 따라 Dockerfile을 정기적으로 재검토하고, 특정 사용 사례에 가장 적합한 접근 방식을 찾기 위해 다양한 방법을 시도하는 것을 두려워하지 마세요.