[Docker] 빌드 & 배포 최적화

이희수·2025년 6월 12일

Cafeboo

목록 보기
2/5

🎯현 상황

카페부 서비스 개발 중 빠른 배포를 위해 Github Actions를 이용한 CD 파이프라인을 구축하였다.

하지만 AI Repository에서 배포시, 빠르면 8분, 느리면 12분까지 소요되면서, 비정상적인 배포 속도를 보여줬다.

이러한 속도는 지속적 배포라는 CD 도입의 취지를 무의미하게 만들고, Agile한 개발 사이클에도 영향을 줄 수 있다고 판단하여 원인을 찾아보았다.

🔍원인

기존에 작성했던 Dockerfile

# Python 3.12.7 기반 이미지
FROM python:3.12.7-slim

# 작업 디렉토리 생성
WORKDIR /app

# 필수 시스템 유틸 설치
RUN apt-get update && apt-get install -y curl tar && rm -rf /var/lib/apt/lists/*

# 구글 관련 패키지 설치
RUN pip install google-genai

# requirements.txt 복사 및 설치
COPY ai_project/requirements.txt ./requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

# 전체 코드 복사
COPY . .

# embedding_model.tar.gz 다운로드 및 압축 해제
RUN curl -L -o embedding_model.tar.gz https://storage.googleapis.com/ai_model_cafeboo/embedding_model.tar.gz && \
    tar -xzf embedding_model.tar.gz -C /app/ai_project/models && \
    rm embedding_model.tar.gz

# moderation_model 추가
RUN curl -L -o /app/ai_project/models/best_model.pt https://storage.googleapis.com/ai_model_cafeboo/moderation_model/best_model.pt


# FastAPI 실행 (내부 통신용)
CMD ["uvicorn", "ai_project.main:app", "--host", "0.0.0.0", "--port", "8000", "--log-level", "debug"]

기본적인 흐름은 아래와 같다.

  1. 필수 시스템 패키지 및 Python 의존성 설치
  2. 전체 프로젝트 코드 복사
  3. 사전 학습된 임베딩, 검열 모델 다운로드 및 압축 해제 (github 용량 이슈)
  4. 애플리케이션 실행 환경 설정

코드를 살펴보면 알 수 있듯이 해당 Dockerfile은 단일 스테이지에서 모든 작업을 순차적으로 수행하고 있다.

💡스테이지란?

Dockerfile에서 스테이지(Stage)란, FROM 명령어로 시작되는 빌드 단계를 의미한다.

Docker는 각 FROM 블록을 하나의 스테이지로 간주하며, 멀티스테이지 빌드에서는 여러 스테이지를 순차적으로 실행하고, 최종적으로 필요한 파일만 추려서 최종 이미지에 포함할 수 있다.

멀티 스테이지를 이용하여 스테이지를 나누면 Docker 빌드를 크게 3가지 측면에서 최적화 할 수 있다.

1. 불필요한 파일을 최종 이미지에서 제거

단일 스테이지: 모델 압축 파일, 빌드 도구, 캐시 파일 등 빌드에만 필요한 파일도 전부 포함되므로 이미지 용량이 커짐

멀티 스테이지: 최종 실행에 필요한 산출물만 선택해서 복사하여 필터링

# base (빌드 전용)
FROM node:18 AS build
RUN npm install && npm run build

# final (실행용)
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html

-> node, npm, node_modules 는 결과물에 포함되지 않음

2. Docker 캐시 효율 증가

FROM openjdk:8-jdk-alpine

ARG JAR_FILE=target/*.jar

COPY ${JAR_FILE} app.jar

ENTRYPOINT ["java", "-jar", "/app.jar"]

위와 같은 Dockerfile이 있을때, Docker명령어 단위로 Layer를 쌓으면서 이미지를 생성한다.

하지만 매번 이 Layer를 다 실행하면 속도가 매우 느리기 때문에, Docker는 Docker Cache를 통해 문제를 해결한다.

Layer에서 변한 것이 없다면 기존의 Layer를 재사용해서 build 과정에서 속도를 높이고, 중복된 Layer를 방지하여 저장공간의 효율을 높인다.

예를 들어 COPY ${JAR_FILE} app.jar 의 경우 app.jar이 변경되지 않으면 이전의 Cache를 적용하여 기존 레이어를 덮어쓰는 방식이다.

# 빌드 전용
FROM gradle:7.6-jdk17 AS builder
COPY . /app
WORKDIR /app
RUN gradle build --no-daemon

# 실행 전용
FROM openjdk:17-alpine
COPY --from=builder /app/build/libs/app.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]

그래서 위와 같이 멀티 스테이지 구조를 사용하면, builder 스테이지에서 COPY . /app 에서 파일이 변경되지 않으면 Gradle 빌드 결과물(app.jar) 도 캐시가 적용된다.

즉, COPY --from=builder는 출력 결과물만 가져오며, 실행 이미지 입장에서는 Gradle이나 소스코드 변경과 완전히 분리된 상태이므로 캐시 적중률이 높아지게 된다.

3. 실행 이미지에서 보안 위험 최소화

curl, tar, apt, gcc, wget 등은 빌드에는 필요하지만 실행 환경에는 불필요하고 위험 요소이기에, 멀티 스테이지를 통해 이런 툴이 포함되지 않는 안전한 프로덕션 이미지를 만들 수 있다.

예를 들어, apt, bash, vim 등 관리용 툴이 남아있으면 의도치 않게 내부 구조 노출 위험이 있으며, gcc, make 등이 있다면 악성 코드를 컨테이너 안에서 실시간으로 컴파일하거나 실행할 수 있다.

🛠️해결방법

기존의 Dockerfile을 아래와 같은 멀티 스테이지 환경으로 분리하였다.

1. Base: OS + Python 환경

FROM python:3.12.7-slim AS base
WORKDIR /app
RUN apt-get update \
  && apt-get install -y curl tar \
  && rm -rf /var/lib/apt/lists/*

Python + 기본 유틸(curl, tar)만 포함한 최소 베이스

2. Deps: pip requirements 설치

FROM base AS deps
COPY ai_project/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

의존성 설치만 따로 분리해서, 코드가 바뀌어도 pip install 캐시는 유지됨

3. Models: 모델 파일 다운로드만 수행

FROM deps AS models
WORKDIR /app/ai_project/models
RUN curl -L -o embedding_model.tar.gz https://storage.googleapis.com/ai_model_cafeboo/embedding_model.tar.gz \
 && tar -xzf embedding_model.tar.gz \
 && rm embedding_model.tar.gz
 && curl -L -o best_model.pt https://storage.googleapis.com/ai_model_cafeboo/moderation_model/best_model.pt

tar.gz 같은 중간 압축파일은 이 스테이지에만 존재 → 최종 이미지엔 포함되지 않음

4. Final: 최종 이미지 - 실행에 필요한 것만 복사

FROM models AS final

WORKDIR /app

# (1) 의존성 복사
COPY --from=deps /usr/local/lib/python3.12 /usr/local/lib/python3.12
COPY --from=deps /usr/local/bin /usr/local/bin

# (2) 모델 복사
COPY --from=models /app/ai_project/models /app/ai_project/models

# (3) 코드 복사
COPY . .

# 실행
CMD ["uvicorn", "ai_project.main:app", "--host", "0.0.0.0", "--port", "8000", "--log-level", "debug"]

오직 실행에 필요한 라이브러리, 모델, 코드만 포함

하지만 이것만으로는 유의미하게 배포가 빨라지지 않았다.
원인을 파악하기 위해 로그를 살펴보자.

여전히 필요한 패키지를 다시 다운받느라 오랜 시간이 소요되는것을 확인할 수 있다.
(전체 7m 29s 중 Build & Push Docker Image 단계에서 6m 33s)

왜 캐시가 적용이 되지않았을까?

Github Actions의 러너는 매번 새로운 가상 환경에서 실행된다. 그래서 모든 작업이 새롭게 다시 시작되고, Github에서 일부 환경을 위한 캐싱을 제공하지만, Docker 레이어에 대한 캐싱은 기본적으로 제공되지 않는다.

이때, BuildKit을 사용해서 해당 문제를 해결 할 수 있다.

💡BuildKit이란?

BuildKit은 Docker의 차세대 빌드 엔진으로, 기존 docker build 명령의 성능과 유연성을 극적으로 개선하기 위해 만들어졌다.

BuildKit은 병렬 레이어 빌드, 캐시, 멀티 플랫폼 빌드, 시크릿 관리 등 다양한 추가 기능을 지원한다.

buildx 를 통해서 BuildKit 을 CLI에서 활용할 수 있다.

# BuildKit 기반 buildx 설정
- name: Set up QEMU Buildx
  uses: docker/setup-buildx-action@v2

# Buildx 캐시 디렉토리 설정
- name: Cache Buildx
  uses: actions/cache@v3
  with:
    path: /tmp/.buildx-cache
    key: ${{ runner.os }}-buildx-${{ github.sha }}
    restore-keys: |
      ${{ runner.os }}-buildx-

# 빌드 시 BuildKit 캐시 적용
- name: Build & Push Docker Image
  run: |
    docker buildx build \
      --push \
      --cache-from=type=local,src=/tmp/.buildx-cache \
      --cache-to=type=local,dest=/tmp/.buildx-cache-new,mode=max \
      -t ${REGION}-docker.pkg.dev/${PROJECT_ID}/ai/cafeboo-ai:${{ github.sha }} .

기존 일반적인 docker build 방식에서 buildx를 이용하여 캐싱을 사용하도록 deploy 파일을 변경해주었다.

제일 처음에 빌드시에는, 캐시가 존재하지 않기에 이전과 비슷하게 8분정도 소요됨을 확인할 수 있다.

하지만 이후 다시 빌드하면 캐시가 적용되어 4분 이내로 소요시간이 단축되었다.

로그를 확인해보니 레이어간 캐시가 잘 적용되는 모습이다!

참고

Docker-Cache를-통해-build-최적화-하기

[Docker] Dockerfile - Multi-stage build(멀티스테이지 빌드)

GitHub Actions에서 도커 캐시를 적용해 이미지 빌드하기

profile
백엔드 개발자로 살아남기

1개의 댓글

comment-user-thumbnail
2025년 6월 12일

성실하시네요

답글 달기