Docker 이미지 빌드 시 'ModuleNotFoundError' 해결 방법

DockerBeginner
지금 연습하기

소개

Python 애플리케이션용 Docker 이미지를 빌드할 때, 개발자는 종종 'ModuleNotFoundError' 메시지를 마주하게 됩니다. 이 오류는 Python 이 애플리케이션에 필요한 모듈 또는 패키지를 찾을 수 없을 때 발생합니다. Docker 초보자에게는 이 문제를 해결하는 것이 특히 어려울 수 있습니다.

이 실습에서는 간단한 Python 애플리케이션을 생성하고, Docker 로 컨테이너화하여 ModuleNotFoundError 를 경험하고, 이를 해결하는 실용적인 방법을 배웁니다. 이 과정을 통해 Docker 이미지에서 Python 종속성을 적절하게 관리하고 프로젝트에서 이 흔한 문제를 피하는 방법을 이해하게 될 것입니다.

간단한 Python 애플리케이션 생성

간단한 Python 애플리케이션을 생성하고 Docker 를 설정하여 실행해 보겠습니다. 이를 통해 Docker 환경에서 ModuleNotFoundError 가 어떻게 발생하는지 이해할 수 있습니다.

Python 애플리케이션 구조 이해

먼저, 프로젝트 디렉토리를 생성하고 해당 디렉토리로 이동해 보겠습니다.

mkdir -p ~/project/docker-python-app
cd ~/project/docker-python-app

이제 타사 모듈을 가져오는 간단한 Python 애플리케이션을 만들어 보겠습니다. 두 개의 파일을 생성합니다.

  1. 메인 애플리케이션 파일
  2. 종속성을 나열하는 requirements 파일

메인 애플리케이션 파일을 생성합니다.

nano app.py

app.py에 다음 코드를 추가합니다.

import requests

def main():
    response = requests.get("https://www.example.com")
    print(f"Status code: {response.status_code}")
    print(f"Content length: {len(response.text)} characters")

if __name__ == "__main__":
    main()

이 간단한 스크립트는 requests 라이브러리를 사용하여 example.com 에 HTTP 요청을 보내고 응답에 대한 몇 가지 기본 정보를 출력합니다.

이제 requirements 파일을 생성해 보겠습니다.

nano requirements.txt

requirements.txt에 다음 줄을 추가합니다.

requests==2.28.1

기본 Dockerfile 생성

이제 ModuleNotFoundError 를 보여주는 간단한 Dockerfile 을 생성해 보겠습니다.

nano Dockerfile

Dockerfile 에 다음 내용을 추가합니다.

FROM python:3.9-slim

WORKDIR /app

COPY app.py .

## We're intentionally NOT copying or installing requirements
## to demonstrate the ModuleNotFoundError

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

이 Dockerfile 은 다음과 같습니다.

  • Python 3.9 slim 이미지를 기본 이미지로 사용합니다.
  • 작업 디렉토리를 /app으로 설정합니다.
  • 애플리케이션 파일을 복사합니다.
  • 애플리케이션을 실행할 명령을 지정합니다.

의도적으로 requirements.txt 파일을 복사하거나 종속성을 설치하지 않았다는 점에 유의하십시오. 이렇게 하면 컨테이너를 실행하려고 할 때 ModuleNotFoundError 가 발생합니다.

Docker 이미지 빌드 및 실행

Docker 이미지를 빌드해 보겠습니다.

docker build -t python-app-error .

다음과 유사한 출력을 볼 수 있습니다.

Sending build context to Docker daemon  3.072kB
Step 1/4 : FROM python:3.9-slim
 ---> 3a4bac80b3ea
Step 2/4 : WORKDIR /app
 ---> Using cache
 ---> a8a4f574dbf5
Step 3/4 : COPY app.py .
 ---> Using cache
 ---> 7d5ae315f84b
Step 4/4 : CMD ["python", "app.py"]
 ---> Using cache
 ---> f5a9b09d7d8e
Successfully built f5a9b09d7d8e
Successfully tagged python-app-error:latest

이제 Docker 컨테이너를 실행해 보겠습니다.

docker run python-app-error

다음과 유사한 오류 메시지를 볼 수 있습니다.

Traceback (most recent call last):
  File "/app/app.py", line 1, in <module>
    import requests
ModuleNotFoundError: No module named 'requests'

이것이 이 실습에서 중점을 두고 있는 ModuleNotFoundError 입니다. 이 오류는 Docker 이미지에 필요한 requests 모듈을 포함하지 않았기 때문에 발생합니다.

ModuleNotFoundError 이해 및 해결

ModuleNotFoundError 를 경험했으므로, 왜 발생했는지 그리고 어떻게 해결해야 하는지 알아보겠습니다.

Docker 에서 ModuleNotFoundError 가 발생하는 이유

ModuleNotFoundError 는 Docker 에서 몇 가지 일반적인 이유로 발생합니다.

  1. 종속성 설치 누락: Docker 이미지에 필요한 Python 패키지를 설치하지 않았습니다.
  2. 잘못된 PYTHONPATH: Python 인터프리터가 예상 위치에서 모듈을 찾을 수 없습니다.
  3. 파일 구조 문제: 애플리케이션 코드 구조가 가져오기 (import) 가 수행되는 방식과 일치하지 않습니다.

이 경우, Docker 이미지에 requests 패키지를 설치하지 않았기 때문에 오류가 발생했습니다. 로컬 개발 환경에서는 이 패키지를 전역적으로 설치했을 수 있지만, Docker 컨테이너는 격리된 환경입니다.

방법 1: Dockerfile 에서 pip 를 사용하여 종속성 설치

Dockerfile 을 수정하여 필요한 종속성을 설치해 보겠습니다.

nano Dockerfile

다음 내용으로 Dockerfile 을 업데이트합니다.

FROM python:3.9-slim

WORKDIR /app

COPY app.py .

## Fix Method 1: Directly install the required package
RUN pip install requests==2.28.1

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

이 업데이트된 이미지를 빌드하고 실행해 보겠습니다.

docker build -t python-app-fixed-1 .

패키지 설치를 포함하는 다음과 유사한 출력을 볼 수 있습니다.

Sending build context to Docker daemon  3.072kB
Step 1/5 : FROM python:3.9-slim
 ---> 3a4bac80b3ea
Step 2/5 : WORKDIR /app
 ---> Using cache
 ---> a8a4f574dbf5
Step 3/5 : COPY app.py .
 ---> Using cache
 ---> 7d5ae315f84b
Step 4/5 : RUN pip install requests==2.28.1
 ---> Running in 5a6d7e8f9b0c
Collecting requests==2.28.1
  Downloading requests-2.28.1-py3-none-any.whl (62 kB)
Collecting charset-normalizer<3,>=2
  Downloading charset_normalizer-2.1.1-py3-none-any.whl (39 kB)
Collecting certifi>=2017.4.17
  Downloading certifi-2022.9.24-py3-none-any.whl (161 kB)
Collecting idna<4,>=2.5
  Downloading idna-3.4-py3-none-any.whl (61 kB)
Collecting urllib3<1.27,>=1.21.1
  Downloading urllib3-1.26.12-py2.py3-none-any.whl (140 kB)
Installing collected packages: urllib3, idna, charset-normalizer, certifi, requests
Successfully installed certifi-2022.9.24 charset-normalizer-2.1.1 idna-3.4 requests-2.28.1 urllib3-1.26.12
 ---> 2b3c4d5e6f7g
Removing intermediate container 5a6d7e8f9b0c
Step 5/5 : CMD ["python", "app.py"]
 ---> Running in 8h9i0j1k2l3m
 ---> 3n4o5p6q7r8s
Removing intermediate container 8h9i0j1k2l3m
Successfully built 3n4o5p6q7r8s
Successfully tagged python-app-fixed-1:latest

이제 수정된 컨테이너를 실행해 보겠습니다.

docker run python-app-fixed-1

다음과 유사한 출력을 볼 수 있습니다.

Status code: 200
Content length: 1256 characters

훌륭합니다! 필요한 종속성을 설치했으므로 애플리케이션이 이제 성공적으로 실행됩니다.

방법 2: 종속성 관리를 위해 requirements.txt 사용

패키지를 직접 설치하는 것도 작동하지만, 더 체계적인 종속성 관리를 위해 requirements.txt 파일을 사용하는 것이 더 좋은 방법입니다. Dockerfile 을 업데이트해 보겠습니다.

nano Dockerfile

다음 내용으로 Dockerfile 을 업데이트합니다.

FROM python:3.9-slim

WORKDIR /app

## Copy requirements first to leverage Docker cache
COPY requirements.txt .

## Fix Method 2: Use requirements.txt
RUN pip install -r requirements.txt

## Copy the rest of the application
COPY app.py .

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

이 접근 방식에는 몇 가지 장점이 있습니다.

  • 종속성 관리를 코드와 분리합니다.
  • 종속성을 쉽게 업데이트할 수 있습니다.
  • Docker 이미지 레이어 캐싱 (layer caching) 에 대한 모범 사례를 따릅니다.

이 업데이트된 이미지를 빌드하고 실행해 보겠습니다.

docker build -t python-app-fixed-2 .

이전 빌드와 유사한 출력을 볼 수 있지만, 이번에는 requirements.txt 를 사용합니다.

Sending build context to Docker daemon  4.096kB
Step 1/5 : FROM python:3.9-slim
 ---> 3a4bac80b3ea
Step 2/5 : WORKDIR /app
 ---> Using cache
 ---> a8a4f574dbf5
Step 3/5 : COPY requirements.txt .
 ---> Using cache
 ---> b2c3d4e5f6g7
Step 4/5 : RUN pip install -r requirements.txt
 ---> Running in h8i9j0k1l2m3
Collecting requests==2.28.1
  Using cached requests-2.28.1-py3-none-any.whl (62 kB)
Collecting charset-normalizer<3,>=2
  Using cached charset_normalizer-2.1.1-py3-none-any.whl (39 kB)
Collecting idna<4,>=2.5
  Using cached idna-3.4-py3-none-any.whl (61 kB)
Collecting certifi>=2017.4.17
  Using cached certifi-2022.9.24-py3-none-any.whl (161 kB)
Collecting urllib3<1.27,>=1.21.1
  Using cached urllib3-1.26.12-py2.py3-none-any.whl (140 kB)
Installing collected packages: urllib3, idna, charset-normalizer, certifi, requests
Successfully installed certifi-2022.9.24 charset-normalizer-2.1.1 idna-3.4 requests-2.28.1 urllib3-1.26.12
 ---> n4o5p6q7r8s9
Removing intermediate container h8i9j0k1l2m3
Step 5/5 : COPY app.py .
 ---> t0u1v2w3x4y5
Step 6/6 : CMD ["python", "app.py"]
 ---> Running in z5a6b7c8d9e0
 ---> f1g2h3i4j5k6
Removing intermediate container z5a6b7c8d9e0
Successfully built f1g2h3i4j5k6
Successfully tagged python-app-fixed-2:latest

이제 컨테이너를 실행해 보겠습니다.

docker run python-app-fixed-2

성공적인 동일한 출력을 볼 수 있습니다.

Status code: 200
Content length: 1256 characters

두 가지 다른 방법을 사용하여 ModuleNotFoundError 를 성공적으로 해결했습니다!

ModuleNotFoundError 방지를 위한 모범 사례

이제 즉각적인 문제를 해결했으므로, Docker 이미지에서 ModuleNotFoundError 를 방지하기 위한 몇 가지 모범 사례를 살펴보겠습니다.

효율적인 빌드를 위한 Docker 캐싱 이해

Docker 는 이미지를 빌드하기 위해 계층화된 접근 방식을 사용합니다. Dockerfile 의 각 명령은 새로운 레이어를 생성합니다. 이미지를 다시 빌드할 때 Docker 는 가능한 경우 캐시된 레이어를 재사용하여 빌드 프로세스를 크게 가속화할 수 있습니다.

Python 애플리케이션의 경우 다음을 통해 캐싱을 최적화할 수 있습니다.

  1. 애플리케이션 코드를 복사하기 전에 requirements 를 복사하고 설치합니다.
  2. 자주 변경되는 파일 (예: 애플리케이션 코드) 을 나중 레이어에 유지합니다.

이러한 모범 사례를 따르도록 Dockerfile 을 업데이트해 보겠습니다.

nano Dockerfile

다음 최적화된 내용으로 Dockerfile 을 업데이트합니다.

FROM python:3.9-slim

WORKDIR /app

## Copy requirements first for better caching
COPY requirements.txt .

## Install dependencies
RUN pip install --no-cache-dir -r requirements.txt

## Copy application code (changes more frequently)
COPY . .

## Make sure we run the application with Python's unbuffered mode for better logging
CMD ["python", "-u", "app.py"]

이 최적화된 이미지를 빌드해 보겠습니다.

docker build -t python-app-optimized .

그리고 작동하는지 확인하기 위해 실행합니다.

docker run python-app-optimized

동일한 성공적인 출력을 볼 수 있습니다.

Status code: 200
Content length: 1256 characters

.dockerignore 파일 사용

Docker 빌드를 더 효율적으로 만들기 위해 Docker 이미지에 필요하지 않은 파일과 디렉토리를 제외하기 위해 .dockerignore 파일을 사용하는 것이 좋습니다. 이렇게 하면 빌드 컨텍스트 크기가 줄어들고 빌드 성능이 향상됩니다.

.dockerignore 파일을 만들어 보겠습니다.

nano .dockerignore

다음 내용을 추가합니다.

__pycache__
*.pyc
*.pyo
*.pyd
.Python
.git
.gitignore
*.log
*.pot
*.env

더 복잡한 애플리케이션 구조 생성

여러 모듈이 있는 더 큰 애플리케이션의 경우 프로젝트를 올바르게 구성하는 것이 중요합니다. 약간 더 복잡한 예제를 만들어 보겠습니다.

mkdir -p myapp

모듈 파일을 생성합니다.

nano myapp/__init__.py

이 파일은 비워둡니다 (디렉토리를 Python 패키지로 표시하기만 합니다).

이제 몇 가지 기능을 가진 모듈 파일을 생성합니다.

nano myapp/utils.py

다음 코드를 추가합니다.

def get_message():
    return "Hello from myapp.utils module!"

이제 이 모듈을 사용하도록 메인 애플리케이션을 업데이트합니다.

nano app.py

내용을 다음으로 바꿉니다.

import requests
from myapp.utils import get_message

def main():
    response = requests.get("https://www.example.com")
    print(f"Status code: {response.status_code}")
    print(f"Content length: {len(response.text)} characters")
    print(get_message())

if __name__ == "__main__":
    main()

업데이트된 애플리케이션을 빌드하고 실행합니다.

docker build -t python-app-modules .
docker run python-app-modules

사용자 정의 메시지를 포함하는 출력을 볼 수 있습니다.

Status code: 200
Content length: 1256 characters
Hello from myapp.utils module!

추가 모범 사례

Docker 에서 ModuleNotFoundError 를 방지하기 위한 몇 가지 추가 모범 사례는 다음과 같습니다.

  1. 가상 환경 (Virtual environments): Docker 에서는 엄격하게 필요하지 않지만 (컨테이너가 격리되어 있으므로), 가상 환경을 사용하면 개발과 프로덕션 간의 일관성을 유지하는 데 도움이 될 수 있습니다.

  2. 고정된 종속성 (Pinned dependencies): 서로 다른 환경에서 일관성을 유지하기 위해 항상 종속성의 정확한 버전을 지정하십시오.

  3. 멀티 스테이지 빌드 (Multi-stage builds): 프로덕션 이미지의 경우, 필요한 종속성만 있는 더 작은 이미지를 만들기 위해 멀티 스테이지 빌드를 사용하는 것을 고려하십시오.

  4. 정기적인 종속성 업데이트: 보안 수정 사항 및 개선 사항을 얻기 위해 종속성을 정기적으로 업데이트하십시오.

이러한 모범 사례를 따르면 Docker 컨테이너에서 ModuleNotFoundError 가 발생할 가능성을 최소화하고 더 효율적이고 유지 관리 가능한 Docker 이미지를 만들 수 있습니다.

요약

이 Lab 에서는 Python 애플리케이션용 Docker 이미지를 사용할 때 ModuleNotFoundError 를 식별, 문제 해결 및 수정하는 방법을 배웠습니다. 다음 사항에 대한 실질적인 경험을 얻었습니다.

  • 기본 Python 애플리케이션을 생성하고 Docker 로 컨테이너화
  • Docker 환경에서 ModuleNotFoundError 가 발생하는 이유 이해
  • 직접 패키지 설치 및 requirements.txt 를 사용하여 종속성 문제 해결
  • 적절한 레이어 캐싱 및 파일 구조와 같은 Docker 모범 사례 구현
  • 여러 모듈을 사용하여 더 복잡한 애플리케이션 구조 생성
  • .dockerignore 를 사용하여 Docker 빌드 최적화

이러한 기술은 Python 애플리케이션에 대해 더 안정적이고 유지 관리 가능한 Docker 이미지를 만드는 데 도움이 됩니다. 이 Lab 에서 다룬 모범 사례를 따르면 ModuleNotFoundError 와 같은 일반적인 함정을 피하고 Docker 개발 워크플로우를 최적화할 수 있습니다.

컨테이너화된 애플리케이션을 사용할 때는 적절한 종속성 관리가 중요하다는 것을 기억하십시오. 항상 Docker 이미지에 필요한 모든 종속성, 적절하게 구조화된 코드, 효율성 및 유지 관리성을 위한 모범 사례가 포함되어 있는지 확인하십시오.