BuildKit cache mount, CI에서도 진짜 캐시 되게 하기

CI에서 Docker 빌드 시간이 아깝다는 얘기를 최근에도 팀에서 몇 번 들었다. 로컬에선 5초 만에 끝나는데 GitHub Actions에서만 3분씩 걸리는, 그 짜증나는 상황. 오늘 알게 된 건 아니지만 의외로 모르는 분들 꽤 많더라. 짧게 정리해둔다.
문제는 러너가 매번 새거라는 점
BuildKit의 RUN --mount=type=cache는 원래 로컬에서 잘 돈다. 예를 들면 이런 식.
# syntax=docker/dockerfile:1.7
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY . .
RUN --mount=type=cache,target=/app/node_modules/.cache \
npm run build
로컬에선 .npm이랑 webpack/vite 캐시가 남아 있어서 두 번째 빌드부터 눈에 띄게 빨라진다. 근데 GitHub Actions 러너는 매번 초기화되니까 cache mount도 결국 비어있는 상태로 시작한다. 이게 함정.
해결: cache-from/to 를 붙여준다
캐시 mount 자체를 registry나 GitHub Actions 캐시에 밀어넣는 방식이다.
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/org/app:latest
cache-from: type=gha
cache-to: type=gha,mode=max
mode=max가 중요하다. 기본값(mode=min)은 최종 스테이지만 캐시하는데, cache mount 내용은 중간 레이어에 붙어 있어서 min으론 안 올라간다. 이걸 몰라서 "왜 cache-to 붙였는데도 안 빨라지지?" 하는 경우가 대부분이다. 최근 6개월 동안 팀에서 두 번 봤다.
GHA cache 용량 주의
type=gha는 10GB 제한이 있다. 이미지 여러 개 굴는 monorepo면 금방 찬다. 그때는 scope를 나눈다.
cache-from: type=gha,scope=api
cache-to: type=gha,mode=max,scope=api
서비스별로 scope를 다르게 주면 서로 밀어내지 않는다. 아니면 아예 registry 캐시(type=registry,ref=...:buildcache)로 옮기는 것도 방법인데, ECR/GHCR 스토리지 비용이랑 저울질해야 한다.
결론 아닌 결론
정리하면 세 줄:
- cache mount는 로컬에서만 도는 게 기본이다
mode=max안 붙이면 CI에선 반쪽짜리다- 용량 차면 scope로 쪼갠다
이거만 알아도 CI 빌드 시간이 절반 이하로 떨어지는 경우가 많다. 우리 팀은 프론트 빌드가 4분 → 40초 정도로 줄었다. 참고로 요즘 나오는 여러 벤치마크 글에서도 잘 튜닝하면 80% 넘게 단축 가능하다고 하는데, 우리 환경은 그 정도까지는 아니었다. 근데 40초는 확실히 참을 만 하다.