캐시를 이용한 Dockerfile 빌드 시간 단축

seongha_h·2025년 1월 6일

Ticle

목록 보기
3/5

프로젝트 링크입니다.
https://github.com/boostcampwm-2024/refactor-web21-TICLE

문제

GitHub Actions를 사용한 배포 시 Docker 이미지를 빌드하는 과정에서 Mediasoup의 의존성 빌드로 인해 시간이 과도하게 소요되고 있습니다.

먼저 Actions의 로그를 분석한 결과 Dockerfile 의 build 가 오래 걸리는 것을 확인할 수 있었습니다.
그중에서도 dockerfile 빌드 과정이 가장 오래 걸리는 것을 확인할 수 있었습니다.

원인

배포 프로세스가 지연되는 원인은 다음과 같습니다.

  1. Actions의 특성
    GitHub Actions 는 매번 새로운 인스턴스 환경에서 빌드를 진행합니다. 이로 인해 로컬에서 진행하는 것처럼 이전 리소스를 재사용할 수 없습니다.
    따라서 매번 코드와 라이브러리를 다운받고 컴파일하고 빌드하는 과정이 필요합니다.

  2. Mediasoup 의존성 설치 및 빌드 시간
    배포 시마다 Mediasoup을 다운로드하고 컴파일하는 데 많은 리소스가 소요됩니다.
    Mediasoup은 WebRTC 기반의 고성능 SFU(Selective Forwarding Unit) 라이브러리로, 네이티브 코드를 포함하고 있어 컴파일 과정이 필수적입니다.
    이에 따라 배포 작업마다 약 10분 이상을 기다려야 하며, 이는 개발 및 배포 프로세스의 생산성을 저하시키고 있습니다.

해결 과정

1. 베이스 이미지 생성

docker를 이용하여 빌드하고 있으므로 Mediasoup이라는 라이브러리를 설치해 둔 이미지를 재사용하면 어떨까? 라는 생각으로 진행하였습니다.

베이스 이미지를 이용하여 라이브러리 설치시간은 20초에서 2초로 감소시킬 수 있었습니다.
그러나 MediaSoup을 컴파일하고 빌드하는 시간은 단축하지 못하여 빌드 시간에 큰 영향을 주지 못하였습니다.

2. 캐싱

위에서 보았듯 Mediasoup은 네이티브 모듈로, 소스 코드를 컴파일하고 빌드해야 사용 가능합니다.
이는 Docker 이미지 빌드 중 많은 시간이 소요되는 주요 원인입니다.

이전 빌드의 결과물을 캐싱하면, 동일한 소스 코드에서는 재컴파일을 방지하여 시간을 절약할 수 있습니다.

이를 위해 dockerfile에 캐싱을 적용하게 되었습니다.

캐싱 적용하기

Actions 구성

기본적으로 Actions는 실행 시 새로운 인스턴스에서 동작하고 종료됩니다. 따라서 로컬 캐싱이 유지되지 않기 때문에 외부 캐싱을 이용해야만 합니다.

docker에서 지원하는 cache with GitHub Actions를 이용하였습니다. 아래 코드에서 볼 수 있듯 gha 옵션을 이용하여 GitHub의 Caches를 이용할 수 있습니다.

https://docs.docker.com/build/ci/github-actions/cache/

Actions 파일의 일부입니다.

- name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: 🐳 Docker 빌드 및 푸시
        uses: docker/build-push-action@v6
        with:
          context: .
          file: apps/media/Dockerfile
          push: true
          tags: ${{secrets.DOCKER_REGISTRY_ACCESS_KEY}}/media:${{ github.sha }},${{secrets.DOCKER_REGISTRY_ACCESS_KEY}}/media:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

Dockerfile

먼저 Dockerfile에 캐싱을 적용하려면 어떻게 동작하는지 이해해야 합니다.

Dockerfile 이미지 레이어

이미지의 레이어는 읽기 전용 파일 시스템의 스냅샷입니다.
이미지는 변경이 불가능하며 새 이미지를 만들거나 그 위에 변경사항을 추가하는 것만 가능합니다.
각 레이어에는 추가, 삭제, 수정 등 파일의 변경사항이 포함되어 있습니다.

Dockerfile에서 레이어를 나누는 기준은 위에서 말한 대로 추가, 삭제, 수정 등 파일의 변경 사항이 있을 때입니다. (FROM, LABEL, COPY, RUN, CMD등)
그리고 Dockerfile은 위에서부터 순서대로 레이어를 쌓으면서 동작합니다. 즉, 바로 이전의 레이어에 다음 레이어가 의존하는 형태입니다.

이 예시에서는 Dockerfile이 어떻게 작성되어 있는지 보여줍니다.

# 베이스 이미지 설정
FROM node:18-alpine

# 작업 디렉터리 설정 
WORKDIR /app

# 환경변수 복사
COPY ./.env ./src/.env

# 의존성 설치
RUN npm install

# 시작
CMD ["npm", "start"];

Dockerfile 캐싱 기준

위에서 말한 대로 Dockerfile은 순서대로 한 단계씩 레이어를 쌓으면서 진행됩니다.
Docker는 이 레이어를 기준으로 캐싱을 진행합니다. 따라서 이전 레이어의 캐시가 무효화 되면 이후 빌드과정의 캐시도 무효화 됩니다.
Dockerfile의 명령이 변경되거나, 순서가 변경되어도 캐시가 무효화 됩니다.

그렇기에 Dockerfile의 명령 순서가 중요합니다. 캐시의 효율성을 증가시키기 위해 잘 변경되지 않는 명령을 위쪽에 배치하는 것이 좋습니다.

또한, Dockerfile 명령어마다 캐싱되는 기준이 다릅니다.

  • FROM : 베이스 이미지가 변하지 않는 한 캐시가 유지됩니다.
  • COPY, ADD : 복사하는 파일이나 디렉터리가 변경되면 캐시가 무효화됩니다. (예: 파일의 내용이 변경되거나 파일이 추가/삭제될 때)
  • RUN : 실행되는 명령의 결과가 변경되면 캐시가 무효화됩니다. (예: 패키지 설치, 환경 변수 변경 등)
  • CMD : 캐싱에 포함되지 않으며, 언제나 마지막에 실행됩니다.
  • ENV : 환경 변수가 설정되면 해당 레이어는 캐시 됩니다.
  • EXPOSE : 단순히 메타데이터를 설정하여 캐시에 영향을 주지 않습니다.

멀티 스테이지 빌드

멀티 스테이지 빌드를 이용하여 도커 이미지의 크기를 줄이고, 빌드 속도를 개선할 수 있습니다.
멀티 스테이지 빌드를 사용하면 Dockerfile에서 여러 빌드 단계를 정의할 수 있으며, 각 단계에서 생성된 중간 산출물만을 최종 이미지로 이동시킬 수 있습니다.

결과적으로 빌드 환경과 실행 환경을 분리하여 필요한 파일만 최종 이미지에 포함합니다. 따라서 도커 이미지를 빌드하고 배포하는 속도를 향상하고, 저장 공간을 절약할 수 있습니다.

dockerfile 구성

다음과 같이 dockerfile을 구성하였습니다.

멀티 스테이지 빌드 적용

runner 단계에서 불필요한 의존성 제거

build 단계에서 사용되었지만 runner 단계에서 사용되지 않는 의존성이 있을 수 있습니다.
예를들면 다음과 같습니다.

libgcc , libstdc++6 등의 의존성은 build시에만 사용되고 있습니다. 이는 runner 단계에 포함된다면 사용되지 않는데 이미지 크기만 증가시키는 단점이 있으므로 제거합니다.

FROM node:18<-bullseye-slim AS base

FROM base AS builder

RUN apt-get update \
 && apt-get install -y \
 python3 \
 python3-pip \
 build-essential \
 linux-headers-amd64 \
 libgcc1 \
 libstdc++6 \
 ninja-build \
 && rm -rf /var/lib/apt/lists/*

WORKDIR /app... 생략

FROM base AS runner

RUN apt-get update \
 && apt-get install -y \
 libc6-dev \
 python3 \
 python3-pip \
 build-essential \
 ffmpeg \
 && rm -rf /var/lib/apt/lists/*

WORKDIR /app... 생략

runner 단계의 dockerfile 순서

dockerfile 은 위에서 부터 차례로 이미지화 됩니다.
이전 명령에서 명령어가 변경되거나, 코드가 변경된다면 캐시를 무효화하고 새로 이미지를 생성합니다. 따라서 변경이 적은 명령을 상위에 위치시켜야 합니다.

하지만, 현재는 runner 단계의 최적화가 무의미 합니다.
Dockerfile을 최적화 하는 이유는 코드의 변경사항을 빠르게 배포 하여 개발 생산성을 증대하는 것입니다. 따라서 build 단계에서 코드의 변경이 자주 발생한다고 가정하였습니다.
그렇다면 runner 단계의 캐싱은 무효화되기 때문에 runner 단계에서 최적화는 현재로서는 무의미하다고 판단하였습니다.

.Dockerignore 적용

COPY . .를 이용하여 파일 변경을 감지하여 캐시를 적용할 수 있도록 하였습니다.
그러나 모노레포로 구성되어 있기에 . 경로에는 ./apps/web 과 같은 FE 코드도 포함되어 있습니다. 이 FE 코드는 변경되더라도 현재 캐싱을 적용한 media 서버에는 영향을 끼치지 않아야 합니다.

따라서 .Dockerignore 파일을 생성하여 Dockerfile에 포함되지 않도록 하여 캐싱 목록에서 제외하였습니다.
.Dockerignore의 위치는 위의 Dockerfile 과 같은 계층에 존재해야 합니다.

# Dockerfile

FROM node:18-bullseye-slim AS base

FROM base AS builder

RUN apt-get update \
    && apt-get install -y \
    python3 \
    python3-pip \
    build-essential \
    linux-headers-amd64 \
    libgcc1 \
    libstdc++6 \
    ninja-build \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

RUN npm install -g pnpm

ENV PNPM_HOME="/root/.local/share/pnpm"
ENV PATH="${PATH}:${PNPM_HOME}"

RUN pnpm install -g turbo@^2.2.3

COPY . .

RUN turbo prune @app/media --docker

FROM base AS runner

RUN apt-get update \
    && apt-get install -y \
    libc6-dev \
    python3 \
    python3-pip \
    build-essential \
    ffmpeg \ 
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml

RUN npm install -g pnpm

RUN pnpm install --frozen-lockfile

COPY --from=builder /app/out/full/ .

RUN pnpm build:media

COPY --from=builder /app/apps/media/.env /app/apps/media/dist/.env
COPY --from=builder /app/apps/media/.env /app/.env

EXPOSE 3002

ENV NODE_ENV=production

CMD ["node", "/app/apps/media/dist/main.js"]
#.Dockerignore
# ./apps/web 디렉토리 제외
apps/web

결과

캐싱을 적용한 결과, 빌드 성능을 크게 향상시킬 수 있었습니다.

1. 빌드 시간 단축

기존 빌드 시간: 약 7분 26초
캐싱 적용 후: 약 1분 19초
개선율: 약 82%

  • 캐시 미적용
  • 캐시 적용

2. 리소스 절약

의존성 설치 및 파일 복사 단계에서 레이어를 재사용 하였습니다.
이를 통해 불필요한 재컴파일 및 설치 방지하여 설치 및 컴파일 단계에서 사용되는 리소스를 절약할 수 있었습니다.

3. 개발 생산성 향상

빌드 속도 개선으로 인해 테스트 및 배포 단계의 대기 시간이 단축하였습니다.
이를 통해 변경 사항을 빠르게 배포하여 피드백 루프를 줄이고 생산성을 높이는 결과를 기대할 수 있습니다.

향후 개선 사항

  • 캐시 관리
    GitHub Actions에서 제공하는 캐시 파일은 최대 7일까지 유지됩니다. 또한, 하나의 캐시 파일이 최대 10GB까지 지원됩니다.
    따라서, 7일 이내에 배포 작업을 더 진행하여 캐시를 유지하고, 하나의 캐시 파일이 10GB를 초과하지 않도록 주의하여 docker 이미지 레이어를 관리해야 합니다.
profile
https://github.com/Fixtar

0개의 댓글