Docker 멀티 스테이지 빌드

김지우·2025년 7월 24일

배경

도커 이미지에 여러 가지 기능을 넣다 보면 이미지 무거워질 수 있다. 이런 경우 가볍게 이미지를 만들 수 있도록 최적화 시키는 것이 필요한데, 그것을 할 수 있는 방법으로 Multi stage build 라는 방법이 있다.

원래 Dockerfile을 build 할 때 FROM으로 시작하는데, FROM이 2개 이상 나오고 이전에 만들어 놓은 환경에서 필요한 것만 가져다 쓰면 Multi stage build로 볼 수 있다.

이 글에서는 빌더와 러너(fastapi)로 나누어 실행해 보겠다. 구조는 대충 아래와 같다.

fastapi-docker-demo/
├── Dockerfile
├── .dockerignore
├── main.py
└── requirements.txt

main.py와 requirements.txt

main.py

해당 파일은 fastAPI로 API를 만든다고 가정하고 health check 기능을 담은 코드를 사용한다.

from fastapi import FastAPI

app = FastAPI()

@app.get('/health')
def health():
    return {'status': 'ok'}

fastAPI를 사용하니 자연스럽게 의존성 패키지로 fastAPI와 uvicorn을 설치해야 한다.

fastapi==0.106.0
uvicorn[standard]==0.24.0


Dockerfile

# 1단계: 빌드 스테이지 (필요시)
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user -r requirements.txt

# 2단계: 실행 스테이지
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

여기서는 기본적으로 빌드와 러너로 나누어 작업을 하는데, 나누어야 하는 이유는 대략 다음과 같다.

  • 경량화

    • 파일을 실행할 때 빌드 도구를 제거 하니 최종 이미지가 작아져, 배포, 다운로드, 실행시간이 빨라진다.
  • 보안 강화

    • 서빙하는 스테이지에 빌드에 필요한 정보가 없어, 혹시 공격 당했을 때 공격자가 빌드 도구를 악용할 가능성이 줄어든다.
  • 유지 보수 용이

    	- 당연히 스테이지를 나누어 사용하니까. 

해당 코드에서 중요하게 주목해야 하는 것은 두가지로 FROM으로 나뉜 두개의 스테이지와 COPY를 통해 빌더에 있는 /root/.local 디렉토리를 러너 실행하는 컨테이너의 /root/.local에 복사하는 부분이다.

빌더에서 설치한 것들을 가져와서 여러번 수행할 필요가 없어져 이미지의 무게가 최적화 될 수 있다.

.dockerignore

__pycache__/
*.pyc
.git
.env

불필요한 확장자의 파일을 사용하지 않게 되어 코드가 이미지 용량 최적화에 도움이 됨


이미지 빌드 및 컨테이너 실행

이미지 빌드

$ docker build -t fastapi-multi .

컨테이너 실행

docker run -d --rm --name myapp -p 8000:8000 fastapi-multi

-d는 백그라운드에서 실행하라는 의미, --rm 해당 컨테이너를 종료하면 지우라는 의미


결과 확인

위의 예시 코드는 잘 동작하면 이렇게 결과가 나옴

profile
프로그래밍 기록 + 공부 기록

0개의 댓글