Docker 빌드 속도를 빠르게 만들기 위해 가장 중요한 개념은 레이어 캐시 인데
Docker는 Dockerfile의 각 명령을 레이어로 나누고, 명령 + 입력 파일 해시를 기준으로 캐시를 재사용함.
아래 예제는 Python 기반 서비스 기준으로 Docker 레이어 캐시가 어떻게 동작하는지 설명한다
도입 배경
내가 만들고 있는것중에 IBIS라고 나라장터 공고를 분석해서 AI를 이용해 분류 , 분석 , 추천 , 응답 하는 서비스를 하나 만들고 있는데, 이 나라장터 공고가 HWP 파일을 이용하다 보니 이거를 노드진영에서 처리하는 라이브러리로
선택한게 LibreOffice 인데 이게 자바로 만들어져있다.
물론 자바로 만든게 문제는 아닌데 이걸 쓰기 위해 jre를 함께 받아와야했고,
liberoffice에서 필요한것만 뽑아다가 넣었지만 기가단위가 나와버린다. (그냥 생짜로받으면 훨씬크다)
그래도 도커 자체에 레이어캐시때문에 괜찮긴 했지만, 서비스에 추가라이브러리를 도입하려면
이걸 빌드하는데 5분에서 10분씩 기다려야했다 (처음 한번만)
그래서 이 liberoffice가 바뀌는것도 없는데 캐시 시켜놓고 쓸 수 없을까해서 도입했다.
변경전
FROM node:18-slim
# Install LibreOffice, JRE, and Fonts for document conversion
# Using leaner packages to reduce image size and build time
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \\
libreoffice-writer \\
libreoffice-java-common \\
libreoffice-l10n-ko \\
default-jre \\
fonts-noto-cjk \\
poppler-utils \\
python3-pip \\
python3-full \\
curl \\
&& pip3 install --no-cache-dir six pyhwp --break-system-packages \\
&& apt-get clean && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY package*.json ./
RUN npm install
# Copy prisma schema and generate client to cache this step
COPY prisma ./prisma/
RUN npx prisma generate
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
변경후
# syntax=docker/dockerfile:1.4
FROM node:18-slim
# Install LibreOffice, JRE, and Fonts for document conversion
# BuildKit cache mount를 사용하여 apt 캐시 재사용 (변경이 없으면 캐시 사용)
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \\
--mount=type=cache,target=/var/lib/apt,sharing=locked \\
apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \\
libreoffice-writer \\
libreoffice-java-common \\
libreoffice-l10n-ko \\
default-jre \\
fonts-noto-cjk \\
poppler-utils \\
python3-pip \\
python3-full \\
curl \\
&& apt-get clean
# BuildKit cache mount를 사용하여 pip 캐시 재사용
RUN --mount=type=cache,target=/root/.cache/pip \\
pip3 install --break-system-packages six pyhwp
WORKDIR /app
COPY package*.json ./
# BuildKit cache mount를 사용하여 npm 캐시 재사용
RUN --mount=type=cache,target=/root/.npm \\
npm install
# Copy prisma schema and generate client to cache this step
COPY prisma ./prisma/
RUN npx prisma generate
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
Docker BuildKit 캐시 마운트 정리
기존 Docker 레이어 캐시 vs BuildKit 방식 비교
Docker 빌드는 기본적으로 레이어 캐시를 사용함.
하지만 레이어가 깨지는 순간, 기존 방식은 모든 패키지를 다시 다운로드해야 함.
BuildKit은 여기서 한 단계 더 나아가 캐시 마운트를 통해 이 문제를 해결함.
아래는 실제 Node + npm + pip + Prisma 조합을 기준으로 정리한 비교임.
기존 방식 (BuildKit 없음)
Docker 레이어 캐시만 사용하는 구조
레이어 구조
Layer 1:FROMnode:18-slim
Layer 2:RUNapt-getinstall...(시스템패키지)
Layer 3:RUNpip3installpyhwp
Layer 4:COPYpackage*.json
Layer 5:RUNnpminstall
Layer 6:COPYprisma
Layer 7:RUNnpxprismagenerate
Layer 8:COPY..←소스전체
시나리오 1: 소스 코드만 변경 (src/server.js 수정)
- Layer 1~7: 레이어 캐시 사용 (스킵)
- Layer 8: COPY . . → 변경 감지
결과
- 매우 빠름
- 약 10초 내외
시나리오 2: package.json 변경
- Layer 1~3: 레이어 캐시 사용
- Layer 4: COPY package*.json → 변경 감지
- Layer 5: RUN npm install → 레이어 무효화
- 네트워크에서 모든 패키지 다시 다운로드
- Layer 6~7: 레이어 캐시 사용
- Layer 8: 레이어 캐시 사용
결과
- npm install 재실행
- 약 1~2분 소요
시나리오 3: Dockerfile 변경 (시스템 패키지 추가)
- Layer 1: 레이어 캐시 사용
- Layer 2: RUN apt-get install → Dockerfile 변경으로 무효화
- apt-get update
- 모든 시스템 패키지 재다운로드
- Layer 3~8: 전부 무효화되어 재실행
결과
- 거의 처음부터 다시 빌드
- 약 10~15분 소요
BuildKit 방식
레이어 캐시 + 캐시 마운트 사용
레이어 구조 (구조는 동일)
Layer 1:FROMnode:18-slim
Layer 2:RUN--mount=type=cache...apt-getinstall
Layer 3:RUN--mount=type=cache...pip3install
Layer 4:COPYpackage*.json
Layer 5:RUN--mount=type=cache...npminstall
Layer 6:COPYprisma
Layer 7:RUNnpxprismagenerate
Layer 8:COPY..
차이점은 RUN 단계에서 캐시 마운트 사용 여부임.
시나리오 1: 소스 코드만 변경
- Layer 1~7: 레이어 캐시 사용
- Layer 8: COPY . . 재실행
결과
- 기존 방식과 동일
- 약 10초
시나리오 2: package.json 변경
- Layer 1~3: 레이어 캐시 사용
- Layer 4: COPY package*.json → 변경 감지
- Layer 5: RUN npm install → 레이어는 무효화되지만
- /root/.npm 캐시 마운트 확인
- 기존 패키지는 캐시에서 재사용
- 신규 패키지만 다운로드
- Layer 6~8: 레이어 캐시 사용
결과
- npm 캐시 재사용
- 약 30초
- 기존 대비 2~4배 빠름
시나리오 3: Dockerfile 변경 (시스템 패키지 추가)
- Layer 1: 레이어 캐시 사용
- Layer 2: RUN apt-get install → 레이어 무효화
- /var/cache/apt 캐시 마운트 확인
- 기존 패키지는 캐시에서 사용
- 신규 패키지만 다운로드
- Layer 3: RUN pip3 install
- /root/.cache/pip 캐시 사용
- pyhwp 재다운로드 없음
- Layer 5: RUN npm install
- /root/.npm 캐시 사용
- 재다운로드 거의 없음
결과
- 시스템 패키지 일부만 다운로드
- 약 3~5분
- 기존 대비 절반 이하로 단축
핵심 차이 요약
항목 기존 방식 BuildKit 방식
| 레이어 캐시 히트 | 빠름 | 빠름 |
| 레이어 무효화 시 | 전체 재다운로드 | 캐시 마운트로 재사용 |
| Dockerfile 변경 | 매우 느림 | 부분 다운로드 |
| npm / pip 변경 | 전체 재다운로드 | 신규만 다운로드 |
BuildKit의 실제 이점이 드러나는 순간
- Dockerfile 수정 (시스템 패키지 추가)
- 기존: apt 전체 재다운로드
- BuildKit: 기존 패키지는 캐시, 신규만 다운로드
- package.json 변경
- 기존: 모든 npm 패키지 재다운로드
- BuildKit: 기존 패키지 재사용
- pip 패키지 변경
- 기존: pyhwp 전체 재다운로드 (대용량)
- BuildKit: 캐시 재사용, 즉시 완료
정리
- Docker 레이어 캐시는 “레이어가 깨지지 않을 때만” 빠름
- BuildKit 캐시 마운트는 레이어가 깨져도 다운로드를 막아줌
- CI/CD 환경, Dockerfile 변경이 잦은 프로젝트일수록 효과가 큼
중요한건 도커 자체의 레이어캐시에만 의존하면, 신규로 뭘 넣다보면 빌드 시간이 오래걸리게 된다는 단점이 있다.
특히나, 내가근무하는 스타트업 특성상 새로운 기능 개발 주기가 빠른데 이런 환경을 미리 준비하는것은
아주 중요하다고 할 수 있다.
다음은 간단하게 도커의 레이어 캐시를 이용하는 방법을 정리한거다.
간단한 파이썬 도커파일로 이해
예시 Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8080
CMD exec uvicorn main:app --host 0.0.0.0 --port ${PORT:-8080}
예시 소스 구조
recommendation-service/
Dockerfile
requirements.txt
main.py
utils.py
Docker 캐시 동작 원리
Docker는 각 레이어를 다음 기준으로 캐시함.
- Dockerfile 명령어 자체
- 해당 명령이 참조하는 파일들의 해시
이 중 하나라도 바뀌면 해당 레이어부터 캐시가 무효화되고, 이후 레이어는 전부 다시 실행됨.
케이스별 캐시 동작 예시
1. main.py만 수정한 경우
변경되는 레이어
- COPY . .
변경되지 않는 레이어
- COPY requirements.txt .
- RUN pip install
결과
- 의존성 설치 재실행 안 됨
- 빌드 매우 빠름
2. requirements.txt 수정한 경우
변경되는 레이어
- COPY requirements.txt .
- RUN pip install
- COPY . .
결과
- 의존성 재설치 발생
- 이후 레이어 전부 재빌드
3. RUN pip install ... 줄을 수정한 경우
변경되는 레이어
- 해당 RUN 명령 이후 모든 레이어
이유
- Docker는 RUN 명령 문자열 자체도 캐시 키로 사용함
결과
- 의존성 재설치 발생
- 전체 재빌드에 가까운 비용 발생
4. COPY requirements.txt . 위치를 아래로 내린 경우
COPY . .
COPY requirements.txt .
RUN pip install -r requirements.txt
결과
- 소스 파일이 변경될 때마다 requirements.txt 해시도 함께 변경됨
- 의존성 설치 레이어가 매번 깨짐
- 캐시 효율 최악
핵심 요약
- Docker는 레이어 단위로 캐시함
- 캐시 판단 기준은 명령어 + 입력 파일 해시
- 소스 코드만 변경되면 보통 마지막 COPY . . 레이어만 갱신됨
- 의존성 파일이 바뀌면 설치 레이어부터 다시 실행됨
- -no-cache-dir는 pip 옵션이며 Docker 캐시와는 무관함
빌드를 더 빠르게 만드는 실전 팁
1. .dockerignore 사용 필수
__pycache__/
.git/
.env
*.log
- 빌드 컨텍스트 크기 감소
- COPY 입력 해시 최소화
- 캐시 적중률 상승
2. BuildKit + pip 캐시 마운트
# syntax=docker/dockerfile:1.6
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \\
pip install -r requirements.txt
COPY . .
특징
- Docker 레이어 캐시가 깨져도 pip 다운로드 캐시는 유지됨
- CI 환경에서 빌드 시간 크게 단축됨
빌드 시 BuildKit 활성화 필요
DOCKER_BUILDKIT=1 docker build .
3. requirements 파일 분리 전략
requirements-base.txt
requirements-prod.txt
COPY requirements-base.txt .
RUN pip install -r requirements-base.txt
COPY requirements-prod.txt .
RUN pip install -r requirements-prod.txt
효과
- 자주 변경되지 않는 의존성 레이어를 깊게 고정 가능
- 대규모 서비스에서 캐시 효율 크게 증가
4. COPY 범위 최소화
COPY main.py utils.py ./
효과
- 불필요한 해시 변경 방지
- 캐시 안정성 증가