[도커 30강] 09강. requirements.txt 기반 Python 이미지 빌드
파이썬 애플리케이션을 도커로 배포할 때 가장 많이 나오는 질문이 있습니다. “왜 어떤 이미지는 금방 빌드되고, 어떤 이미지는 매번 오래 걸릴까?” 원인의 상당수는 requirements.txt를 Dockerfile에서 다루는 순서와 방식에 있습니다. 오늘은 requirements 파일 기반으로 Python 이미지를 안정적으로 빌드하는 방법을, 실무에서 바로 써먹을 수 있는 기준으로 정리해보겠습니다.
핵심은 간단합니다. 의존성 설치 레이어를 최대한 캐시 친화적으로 만들고, 소스 변경과 의존성 변경을 분리하는 것입니다. 이 원칙을 지키면 로컬 개발 속도, CI 빌드 시간, 운영 배포 안정성이 함께 좋아집니다.
핵심 개념
- Python 이미지 빌드의 체감 성능은 대부분
pip install -r requirements.txt레이어 캐시 적중률로 결정됩니다. COPY requirements.txt후RUN pip install ...을 먼저 배치하고, 애플리케이션 소스COPY . .는 뒤로 배치해야 재빌드가 빨라집니다.requirements.txt는 “환경을 재현하기 위한 계약서”입니다. 버전 핀 고정(==)을 습관화해야 팀/CI/운영 간 동작 차이를 줄일 수 있습니다.- 빌드 컨텍스트가 불필요하게 크면 캐시가 자주 깨집니다.
.dockerignore로 로그, 가상환경, git 메타데이터를 제외해야 합니다. python:3.12-slim같은 경량 베이스를 기본으로 사용하되, C 확장 빌드가 필요한 패키지 여부에 따라 빌드 도구 설치 전략을 분리해야 합니다.
기본 사용
예제 1) 가장 표준적인 requirements 기반 Dockerfile
mkdir -p ~/docker100/lesson09/app && cd ~/docker100/lesson09
cat > app/main.py <<'PY'
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def home():
return {"message": "lesson09 requirements build"}
PY
cat > requirements.txt <<'REQ'
fastapi==0.115.6
uvicorn==0.32.1
REQ
cat > Dockerfile <<'EOF_DOCKER'
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r /app/requirements.txt
COPY app/ /app/
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
EOF_DOCKER
docker build -t docker100-lesson09:basic .
docker run --rm -p 8000:8000 docker100-lesson09:basic
설명:
requirements.txt를 먼저 복사하고 설치해서 의존성 레이어 캐시를 확보합니다.- 이후 소스를 복사하므로 코드만 바뀌는 상황에서는 pip 설치를 재실행하지 않을 가능성이 큽니다.
--no-cache-dir는 pip 캐시를 이미지 내부에 남기지 않아 최종 이미지 크기를 줄이는 데 도움이 됩니다.
예제 2) 코드 변경 vs 의존성 변경에 따른 빌드 차이 확인
# 1차 빌드
time docker build -t docker100-lesson09:cache-test .
# 코드만 변경
python - <<'PY'
from pathlib import Path
p = Path('app/main.py')
text = p.read_text()
p.write_text(text + "\n# comment for cache test\n")
PY
# 2차 빌드
time docker build -t docker100-lesson09:cache-test .
# 의존성 변경
printf "\nhttpx==0.28.1\n" >> requirements.txt
# 3차 빌드
time docker build -t docker100-lesson09:cache-test .
설명:
- 코드만 바뀌면 일반적으로
pip install레이어는 캐시 재사용됩니다. requirements.txt가 바뀌면 의존성 레이어가 무효화되어 설치 단계가 다시 실행됩니다.- 이 동작을 팀이 이해하고 있으면 “왜 이번 빌드가 느렸는지”를 쉽게 설명할 수 있습니다.
예제 3) .dockerignore로 캐시 안정화 + 컨텍스트 최적화
cat > .dockerignore <<'EOF_IGNORE'
.git
.gitignore
__pycache__
*.pyc
.venv
venv
.pytest_cache
.mypy_cache
.env
*.log
EOF_IGNORE
# 컨텍스트 전송량 확인을 위해 빌드 로그 비교
docker build --progress=plain -t docker100-lesson09:ignore-on .
설명:
- 로컬 가상환경(
.venv)이나 캐시 폴더가 컨텍스트에 포함되면, 불필요한 파일 변경으로 레이어 무효화가 빈번해집니다. .dockerignore는 빌드 속도뿐 아니라 재현성, 보안(불필요 파일 유출 방지)에도 영향을 줍니다.
예제 4) 운영 친화형 패턴
cat > Dockerfile.prod <<'EOF_DOCKER'
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
RUN useradd -m appuser
COPY requirements.txt /app/requirements.txt
RUN pip install --no-cache-dir -r /app/requirements.txt
COPY app/ /app/
RUN chown -R appuser:appuser /app
USER appuser
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
EOF_DOCKER
docker build -f Dockerfile.prod -t docker100-lesson09:prod .
설명:
- 개발 단계에서는 단순한 Dockerfile로 시작하고, 운영 단계에서 최소 권한 원칙(
USER appuser)을 추가하는 식으로 진화시키면 좋습니다. PYTHONUNBUFFERED=1을 설정하면 로그가 지연 없이 출력되어 운영 관찰성이 좋아집니다.
자주 하는 실수
실수 1) COPY . .를 먼저 두고 나중에 pip install 실행
- 원인: Dockerfile을 직관적으로 작성하다 보니 “소스 전체 복사 → 설치” 순서로 작성함.
- 해결: 의존성 파일(
requirements.txt)을 먼저 복사/설치한 뒤 앱 소스를 복사하는 고정 패턴을 팀 규칙으로 정합니다.
실수 2) requirements 버전을 느슨하게 관리
- 원인: 최신 패키지를 자동으로 받으면 좋겠다는 기대.
- 해결: 운영 재현성을 위해 기본은
==핀 고정으로 관리하고, 업데이트는 정기 배치/PR로 통제합니다.
실수 3) 로컬 가상환경 폴더를 이미지에 함께 복사
- 원인:
.dockerignore미설정 또는 누락. - 해결:
.venv,venv, 캐시 디렉터리, 테스트 산출물을 반드시 제외합니다.
실수 4) 빌드 실패 원인을 패키지 문제로만 단정
- 원인: 네트워크/인덱스/시스템 의존성(libpq-dev 등) 이슈를 구분하지 못함.
- 해결:
--progress=plain로그로 단계별 실패 지점을 확인하고, Python 패키지인지 OS 패키지인지 원인을 분리합니다.
실무 패턴
실무에서 requirements 기반 빌드를 안정화하려면 “파일 분리 + 단계 분리 + 변경 주기 분리” 세 가지를 동시에 잡아야 합니다.
첫째, 파일 분리입니다. 단일 requirements.txt로 시작하더라도 프로젝트가 커지면 requirements/base.txt, requirements/dev.txt, requirements/prod.txt처럼 목적별 분리를 고려합니다. 이렇게 나누면 개발 도구(black, pytest 등) 때문에 운영 이미지가 불필요하게 무거워지는 문제를 줄일 수 있습니다.
둘째, 단계 분리입니다. Dockerfile에서는 의존성 설치 단계를 소스 복사 단계보다 앞에 두고, 가능한 한 변동성이 낮은 명령을 상단에 배치합니다. 반대로 변경이 잦은 소스 복사와 메타데이터 주입은 하단으로 내립니다. 이 설계는 캐시 효율을 높이는 가장 즉각적인 방법입니다.
셋째, 변경 주기 분리입니다. 의존성 업데이트와 기능 개발을 같은 커밋/같은 배포에 섞지 않는 습관이 중요합니다. 코드가 깨졌는지, 의존성이 깨졌는지 원인을 빠르게 분리할 수 있기 때문입니다. 팀 운영에서는 “의존성 업데이트 전용 PR”을 따로 운영하면 장애 분석 속도가 크게 올라갑니다.
CI 관점에서는 캐시 저장소를 활용해 빌드 시간을 줄일 수 있지만, 캐시가 낡아 생기는 문제도 관리해야 합니다. 예를 들어 주 1회 클린 빌드를 강제로 수행해 숨은 의존성 문제를 조기에 발견하는 정책이 효과적입니다. 로컬에서는 빠르게, CI에서는 신뢰성 있게라는 기준을 분리해서 보는 것이 좋습니다.
보안 측면도 꼭 챙겨야 합니다. requirements에 직접 사설 토큰 URL을 넣거나, Dockerfile에 인증 정보를 하드코딩하는 패턴은 금물입니다. 인증이 필요한 패키지 저장소를 쓰는 경우에는 빌드 시크릿/CI 시크릿 변수를 통해 주입하고, 이미지/로그에 남지 않도록 별도 절차를 둬야 합니다.
정리하면 requirements 기반 빌드는 단순한 문법 문제가 아니라, 재현성·속도·보안·운영성을 함께 다루는 설계 문제입니다. 처음부터 완벽할 필요는 없지만, 오늘 정리한 패턴을 기본값으로 잡아두면 프로젝트가 커질수록 차이가 크게 납니다.
오늘의 결론
한 줄 요약: requirements.txt를 먼저 복사/설치하고 소스는 나중에 복사하라 — 이것이 Python 도커 빌드 속도와 재현성을 동시에 지키는 핵심 패턴이다.
여기에 .dockerignore, 버전 핀 고정, 비루트 실행까지 더하면 “일단 돌아가는 이미지”에서 “팀이 믿고 배포하는 이미지”로 올라갈 수 있습니다.
연습문제
- 현재 프로젝트 Dockerfile에서
COPY . .와pip install의 순서를 바꿔 보고, 코드 변경 시 빌드 시간 차이를 측정해보세요. requirements.txt의 패키지 버전을 하나 올린 뒤, 어떤 레이어부터 다시 빌드되는지--progress=plain로그로 분석해보세요..dockerignore에 일부 항목을 의도적으로 제거한 후 컨텍스트 전송량과 캐시 적중률 변화를 비교해보세요.
이전 강의 정답
8강 연습문제 해설:
APP_VERSION처럼 빌드 시점에만 필요한 값은ARG로 주입하고, 런타임에서 지속되어야 하는 값은ENV로 분리해야 합니다.ARG는 기본적으로 컨테이너 실행 시 환경변수로 자동 노출되지 않습니다.ENV PORT=8000처럼 기본값을 선언한 뒤docker run -e PORT=9000으로 덮어쓰면, 컨테이너 내부 최종 값은 9000이 됩니다. 즉 Dockerfile의ENV는 기본값이며, 실행 시점 값이 우선합니다.- 민감값이
ARG/ENV에 하드코딩되어 있다면 시크릿 주입 방식으로 전환해야 합니다. 특히 토큰/비밀번호는 이미지 히스토리나 로그에 남을 수 있으므로 “이미지에 남아도 되는가?” 기준으로 반드시 재검토해야 합니다.
실습 환경/재현 정보
- OS: macOS 15+ 또는 Ubuntu 22.04+
- Docker 버전: Docker Engine/Desktop 25.x 이상
- Python 베이스 이미지:
python:3.12-slim - 실행 순서:
- 샘플 앱(
app/main.py)과requirements.txt작성 - Dockerfile에서 requirements 선복사/선설치 패턴 적용
- 1차 빌드 후 코드만 변경하여 2차 빌드
- requirements 변경 후 3차 빌드
.dockerignore적용 전/후 비교
- 샘플 앱(
- 재현 체크:
- 코드 변경 시
pip install레이어가 캐시 재사용되는가? - requirements 변경 시 의존성 설치 레이어가 재실행되는가?
- 이미지 내부에 불필요한 로컬 파일(venv, git, 로그)이 포함되지 않는가?
- 컨테이너 로그가 즉시 출력되고(
PYTHONUNBUFFERED=1), 앱이 정상 기동되는가?
- 코드 변경 시