- Published on
Docker BuildKit 캐시 안 먹을 때 --mount=cache 실전
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서론 없이 바로 결론부터 말하면, BuildKit을 켰는데도 캐시가 안 먹는다고 느끼는 대부분의 원인은 레이어 캐시와 캐시 마운트를 같은 것으로 착각하거나, 캐시가 깨지는 Dockerfile 구조(특히 COPY . .의 위치) 때문입니다.
BuildKit의 --mount=type=cache는 “레이어를 재사용”하는 기능이 아니라, RUN 단계에서 사용하는 디렉터리를 빌드 간에 재사용하도록 해주는 기능입니다. 즉, 패키지 매니저 다운로드 캐시, 컴파일러 캐시, 테스트/번들러 캐시 같은 “작업 캐시”를 살리는 도구입니다.
이 글에서는 다음을 다룹니다.
- BuildKit 캐시가 “안 먹는 것처럼 보이는” 대표 원인
--mount=type=cache를 어디에, 어떻게 붙여야 효과가 나는지- Node.js, Python, Go 등에서 바로 써먹는 패턴
- CI에서 캐시가 매번 날아가는 상황을 줄이는 방법
참고로 CI 캐시 자체가 안 먹는 문제는 Docker BuildKit과 별개로 파이프라인 설정(키, 경로, 권한) 이슈일 수 있으니, GitLab 환경이라면 GitLab CI 캐시 안 먹을 때 - 키·경로·권한 점검도 같이 확인하는 게 좋습니다.
BuildKit 캐시의 2가지: 레이어 캐시 vs cache mount
1) 레이어 캐시(전통적인 Docker 캐시)
Dockerfile의 각 명령(FROM, COPY, RUN 등)은 레이어를 만들고, 입력이 동일하면 이전 레이어 결과를 재사용합니다.
- 장점: 가장 강력하고 빠름(해당 레이어 자체를 통째로 재사용)
- 단점: 입력이 조금만 바뀌어도(특히
COPY . .) 레이어가 무효화됨
2) --mount=type=cache(BuildKit의 캐시 볼륨)
RUN 안에서만 사용 가능한 “빌드 전용 캐시 디렉터리”를 제공합니다.
- 장점: 레이어가 무효화되어도(즉,
RUN이 다시 실행되어도) 다운로드/컴파일 캐시를 재사용 가능 - 단점:
RUN이 다시 실행되는 건 막지 못함(속도만 줄여줌)
즉, “레이어 캐시가 깨졌는데도 다운로드는 다시 안 하게 만들고 싶다”가 --mount=cache의 핵심입니다.
BuildKit을 제대로 켰는지 먼저 확인
BuildKit 문법(RUN --mount=...)이 먹히려면 BuildKit이 활성화되어야 합니다.
로컬에서:
DOCKER_BUILDKIT=1 docker build -t app:dev .
Dockerfile 상단에 syntax 지시자를 넣으면 BuildKit 문법을 더 안정적으로 씁니다.
# syntax=docker/dockerfile:1.7
FROM ubuntu:24.04
빌드 로그에 #x [stage ...] 같은 BuildKit 스타일 출력이 보이면 대체로 정상입니다.
“캐시가 안 먹는다”의 흔한 원인 6가지
1) COPY . .가 너무 빨리 나온다
가장 흔한 실수입니다. 소스 전체를 먼저 복사하면, 소스 변경이 있을 때마다 이후 모든 RUN 레이어 캐시가 깨집니다.
나쁜 예:
FROM node:20
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
좋은 예(의존성 파일만 먼저 복사):
FROM node:20
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
이렇게만 해도 “레이어 캐시”가 크게 살아납니다.
2) 패키지 매니저 캐시 디렉터리를 컨테이너 레이어에 묻어버린다
예를 들어 npm ci는 기본적으로 네트워크 다운로드를 하고, 내부 캐시를 활용합니다. 그런데 레이어 캐시가 깨지면 다시 다운로드합니다. 이때 --mount=cache를 붙이면 다운로드 캐시를 재사용할 수 있습니다.
3) --mount=cache에 id를 안 줘서 캐시가 분산된다
BuildKit은 캐시를 식별하기 위해 id를 사용할 수 있습니다. id를 안 주면 경로 기반으로 동작하지만, 멀티 스테이지/멀티 플랫폼/CI 환경에서 의도치 않게 캐시가 분리되거나 충돌하는 경우가 생깁니다.
4) 캐시 경로를 잘못 잡는다
패키지 매니저마다 캐시 경로가 다릅니다. 예를 들어:
- npm: 보통
/root/.npm또는 사용자 홈의.npm - pnpm: 보통
/pnpm/store또는 설정에 따라 다름 - pip: 보통
/root/.cache/pip - apt:
/var/cache/apt및/var/lib/apt
캐시가 실제로 쓰이는 경로에 마운트해야 체감이 납니다.
5) CI에서 빌더가 매번 바뀐다(캐시 저장소가 없다)
로컬은 빌드 머신이 고정이라 캐시가 남지만, CI는 매 실행마다 깨끗한 VM/컨테이너에서 시작하는 경우가 많습니다. 이때는 BuildKit 캐시를 레지스트리나 로컬 디스크로 export/import 해야 합니다(아래에 예시).
6) 레지스트리 Pull 문제가 빌드 캐시 문제로 오해된다
빌드가 느린 원인이 사실은 베이스 이미지 pull 지연/에러인 경우도 많습니다. 특히 K8s 배포에서 ImagePullBackOff가 섞이면 빌드 캐시를 아무리 잡아도 체감이 없습니다. 레지스트리 인증/레이트리밋 이슈가 의심되면 K8s ImagePullBackOff 401·429 - ECR·레지스트리 제한 해결도 함께 점검하세요.
--mount=type=cache 기본 사용법
형식은 대략 아래처럼 씁니다.
RUN \
some-command
자주 쓰는 옵션:
type=cache: 캐시 마운트id=...: 캐시 식별자(프로젝트/스테이지/아키텍처 기준으로 안정적으로)target=...: 컨테이너 안에서 캐시로 쓸 경로sharing=locked: 동시 빌드에서 경합 줄이기(패키지 매니저에 따라 유용)
주의: 일반 텍스트로 부등호를 쓰면 MDX에서 문제가 될 수 있으니, 비교나 화살표 표기는 항상 인라인 코드로 처리하세요(예: a -> b).
실전 1: Node.js(npm)에서 다운로드 캐시 살리기
아래 패턴이 가장 무난합니다.
# syntax=docker/dockerfile:1.7
FROM node:20-bookworm AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN \
npm ci
COPY . .
RUN npm run build
포인트:
npm ci는package-lock.json이 바뀌지 않으면 레이어 캐시로도 재사용됩니다.- 하지만 소스 변경으로 레이어가 깨져
npm ci가 다시 실행되는 상황에서도,--mount=cache덕분에 다운로드가 크게 줄어듭니다.
pnpm을 쓴다면(store 마운트)
pnpm은 store를 제대로 잡아주는 게 핵심입니다.
# syntax=docker/dockerfile:1.7
FROM node:20-bookworm AS build
WORKDIR /app
RUN corepack enable
COPY package.json pnpm-lock.yaml ./
RUN \
pnpm config set store-dir /pnpm/store \
&& pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
실전 2: Python(pip)에서 휠/다운로드 캐시 살리기
pip는 캐시 디렉터리를 마운트하면 체감이 큽니다.
# syntax=docker/dockerfile:1.7
FROM python:3.12-slim AS build
WORKDIR /app
COPY requirements.txt ./
RUN \
pip install -r requirements.txt
COPY . .
RUN python -m compileall .
추가 팁:
- 가능하면
requirements.txt(또는poetry.lock)만 먼저 복사해 의존성 레이어 캐시를 살립니다. - 네이티브 빌드 의존성이 많은 패키지는 “다운로드 캐시” 외에 컴파일 캐시도 고려해야 합니다(예:
ccache).
실전 3: apt 캐시로 OS 패키지 설치 시간 줄이기
apt-get update는 레이어 캐시가 깨지면 매번 느려집니다. BuildKit 캐시 마운트로 어느 정도 완화할 수 있습니다.
# syntax=docker/dockerfile:1.7
FROM ubuntu:24.04
RUN \
apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
주의할 점:
rm -rf /var/lib/apt/lists/*는 이미지 크기에는 좋지만, 캐시 마운트와 섞이면 기대한 만큼 효과가 안 날 수 있습니다. 그래도 보안/용량 관점에서 유지하는 경우가 많으니, 빌드 시간과 이미지 정책 사이에서 선택하세요.
실전 4: Go 빌드 캐시(GOMODCACHE, GOCACHE)
Go는 캐시가 잘 잡히면 빌드 시간이 극적으로 줄어듭니다.
# syntax=docker/dockerfile:1.7
FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN \
go mod download
COPY . .
RUN \
go build -o /out/app ./...
포인트:
go mod download단계에서 모듈 캐시를 만들고- 실제
go build에서도 같은 캐시를 다시 마운트해야 효과가 유지됩니다.
캐시가 정말 적용되는지 확인하는 방법
1) BuildKit 진행 로그 확인
BuildKit은 각 스텝이 CACHED인지 표시해줍니다.
DOCKER_BUILDKIT=1 docker build --progress=plain -t app:dev .
CACHED로 뜨면 레이어 캐시 적중--mount=cache는 레이어 캐시와 별개라서,RUN이 실행되더라도 내부 다운로드가 빨라지는 형태로 나타납니다(로그에 다운로드가 줄거나, 시간이 짧아짐)
2) 의도적으로 한 파일만 바꿔서 비교
- 소스 파일만 변경: 의존성 설치 단계가 재실행되는지
- lock 파일 변경: 의존성 설치 단계가 재실행되는 건 정상
이 비교를 하면 “레이어 캐시 문제인지” “다운로드 캐시 문제인지”가 분리됩니다.
CI에서 BuildKit 캐시를 살리는 핵심: cache export/import
CI는 빌드 머신이 매번 새로 뜨면 로컬 캐시가 남지 않습니다. 이때는 buildx로 캐시를 외부로 내보내고 다음 빌드에서 다시 가져와야 합니다.
예시(레지스트리에 캐시 저장):
docker buildx build \
--cache-to=type=registry,ref=registry.example.com/myapp:buildcache,mode=max \
--cache-from=type=registry,ref=registry.example.com/myapp:buildcache \
-t registry.example.com/myapp:latest \
--push \
.
이 구성이 없으면 --mount=cache를 잘 써도 “러너가 바뀌면 캐시가 없다”는 현실을 못 이깁니다.
또한 CI에서 레지스트리 인증이 삐끗하면 캐시 가져오기 자체가 실패합니다. GitHub Actions에서 토큰 권한 문제로 403이 나면 GitHub Actions GITHUB_TOKEN 403 권한 오류 해결처럼 권한 스코프/패키지 권한을 먼저 잡아야 캐시 전략이 의미가 있습니다.
자주 묻는 질문(실전에서 많이 터지는 케이스)
Q1. --mount=cache만 쓰면 npm ci 레이어도 캐시되나요?
아니요. --mount=cache는 레이어 캐시를 대체하지 않습니다. npm ci가 실행되는 레이어가 CACHED가 되려면, 그 레이어의 입력(이전 레이어 결과, 복사된 파일 등)이 동일해야 합니다.
다만 레이어가 다시 실행되더라도 다운로드 캐시를 재사용해 시간을 줄여줍니다.
Q2. 왜 어떤 환경에서는 빨라지고 어떤 환경에서는 그대로인가요?
- 로컬은 캐시가 디스크에 남음
- CI는 실행 환경이 매번 초기화됨
- 멀티 플랫폼 빌드(예:
linux/amd64와linux/arm64)는 캐시가 분리될 수 있음
이 차이 때문에 같은 Dockerfile이라도 체감이 달라집니다.
Q3. 캐시 id는 어떻게 정하는 게 좋나요?
대체로 다음 기준이 안전합니다.
- 프로젝트 기준:
myapp-npm - 스테이지 기준:
myapp-npm-build - 아키텍처 분리 필요 시:
myapp-npm-amd64
특히 멀티 플랫폼에서 네이티브 바이너리 캐시가 섞이면 문제가 될 수 있으니, 필요하면 id를 분리하세요.
정리: “캐시 안 먹음”을 해결하는 체크리스트
- Dockerfile에서 의존성 파일만 먼저
COPY했는가 - BuildKit이 실제로 활성화되어 있는가(
--progress=plain로 확인) RUN --mount=type=cache를 “실제로 캐시를 쓰는 경로”에 걸었는가- 캐시
id를 안정적으로 지정했는가 - CI라면
--cache-to/--cache-from로 캐시를 외부 저장소에 유지하고 있는가 - 느린 원인이 레지스트리 pull/인증 문제는 아닌가
위 순서대로만 점검해도, “BuildKit 캐시가 안 먹는다”는 느낌의 대부분은 재현 가능하게 원인을 좁히고 해결할 수 있습니다.