kimyu0218
  • [docker] 이미지 빌드 (빌드 컨텍스트/프로세스/캐싱/멀티 스테이지)
    2025년 09월 15일 04시 20분 06초에 업로드 된 글입니다.
    작성자: @kimyu0218

    도커는 이미지를 만들 때 Dockerfile을 사용한다. Dockerfile은 컨테이너 실행에 필요한 명령어들이 담긴 파일인데, 여기에는 필요한 패키지를 설치하거나 소스 코드를 복사하는 명령어들이 적혀 있다. 근데 이러한 파일들은 어디에서 복사해오는 걸까? 지금부터 빌드 컨텍스트와 이미지 빌드의 전반적인 과정을 살펴보자.

     

    빌드 컨텍스트

    `docker build`로 이미지를 빌드할 때 반드시 `PATH`를 전달해야 한다. 여기서 지정한 `PATH`가 빌드 컨텍스트가 된다. 빌드 컨텍스트는 도커가 이미지 빌드 과정에서 접근할 수 있는 파일들의 작업 공간이다. 우리가 `COPY`, `ADD` 같은 명령어로 가져오는 파일들은 전부 이 빌드 컨텍스트 안에서만 참조할 수 있다.

    Sending buid context to Docker daemon 4 KB

    도커는 이미지를 빌드할 때 가장 먼저 빌드 컨텍스트를 읽어들인다. 따라서 이미지 빌드에 필요한 파일만 있는 디렉토리를 빌드 컨텍스트로 지정하는 것이 바람직하다. 만약 루트 디렉토리를 빌드 컨텍스트로 사용하면 하위 디렉토리도 전부 포함하기 때문에 빌드 속도가 느려진다.

    이를 방지하기 위해 `.gitignore`처럼 `.dockerignore`를 사용하는 방법도 있다. `.dockerignore`를 작성하면 이 파일에 명시된 파일들은 컨텍스트에서 제외된다.

    🚨 `.dockerfile`은 반드시 빌드 컨텍스트 루트에 위치해야 한다. 이미지를 빌드하면 사실상 가장 먼저 `.dockerignore`를 찾고, 이를 바탕으로 빌드 컨텍스트를 읽어들인다.

     

    빌드 프로세스

    이미지는 여러 계층으로 이루어져있다. 일반적으로 `Dockerfile`의 한 줄이 하나의 계층이 된다. (예외로 `ENV`, `WORKDIR` 등은 레이어를 만들지 않는다.)

     

    도커는 Dockerfile을 위에서부터 순서대로 읽으면서 각 명령어를 실행한다. 이때 각 명령어는 다음 과정을 거쳐 하나의 계층이 된다.

    1. 컨테이너 생성 : 현재까지 만들어진 이미지를 기반으로 임시 컨테이너를 띄운다.
    2. 명령어 실행 : 명령어를 컨테이너 안에서 실행한다.
    3. 실행 결과를 새로운 레이어로 커밋 : 실행 결과로 생긴 파일 시스템 변경 내용이 새로운 계층으로 저장된다.
    4. 컨테이너 제거 : 작업을 마친 컨테이너를 삭제한다.
    Step 3/6 : RUN apt-get update
     ---> Running in 123abc456def # 컨테이너 생성
     ---> 789ghi012jkl # 새로운 이미지 레이어 커밋
    Removing intermediate container 123abc456def # 컨테이너 삭제
    🚨 각 스텝은 Dockerfile의 명령어에 해당하고, 명령어가 실행될 때마다 새로운 컨테이너가 하나씩 생성되고 이를 이미지로 커밋한다. 임시 컨테이너를 삭제하기 전에 출력되는 ID가 커밋된 이미지 레이어다.

     

    이미지 캐싱

    도커는 캐싱을 통해 빌드 속도를 높일 수 있다. 이전에 빌드했던 Dockerfile에 같은 내용이 있다면 새로 빌드하지 않고 같은 명령어 줄까지 이전에 사용한 이미지 레이어를 활용한다.

    Step 3/6 : RUN apt-get update
     ---> Using cache
     ---> 123abc456def
    🚨 단순히 Dockerfile의 명령어가 같은지 비교하는 것이 아니라 파일 내용까지 해시로 계산해 비교한다. 따라서 해시값이 달라진다면 캐시가 무효화되어 다시 빌드된다.

     

    멀티 스테이지 빌드

    일반적으로 애플리케이션 빌드는 많은 의존성을 필요로 한다. 그래서 빌드에 필요한 도구나 라이브러리까지 최종 이미지에 포함돼서 예상보다 이미지 크기가 커지는 경우가 많다. 하지만 애플리케이션을 실행할 때는 이런 것들이 전혀 필요하지 않다. 따라서 최종 이미지에서는 빌드에만 쓰였던 것들을 없애는 게 좋은데, 이를 가능하게 해주는 방법이 멀티 스테이지 빌드다.

     

    멀티 스테이지 빌드는 하나의 Dockerfile 안에 여러 개의 FROM 이미지를 정의한다. 빌드와 실행을 두 단계로 나누어보자.

    • 첫 번째 단계에서는 빌드에 필요한 환경을 준비하고 소스 코드를 컴파일한다.
    • 두 번째 단계에서는 첫 번째 빌드 결과물만 가져와서, 최소한의 실행 환경을 구성한다.

    이처럼 빌드에 필요한 이미지와 실행에 필요한 이미지를 분리하여 최종 이미지의 크기를 절약할 수 있다.

    # 빌드 스테이지
    FROM node:18 AS builder
    WORKDIR /app
    COPY package*.json ./
    RUN npm ci
    COPY . .
    
    # 실행 스테이지
    FROM node:18-alpine
    WORKDIR /app
    COPY --from=builder /app ./
    EXPOSE 3000
    CMD ["node", "server.js"]
    댓글