- Published on
Docker 빌드 캐시가 안 먹을 때 - BuildKit 원인 8가지
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
Docker 이미지 빌드가 매번 풀 빌드처럼 느려질 때, 가장 먼저 의심해야 하는 건 “캐시가 깨졌다”가 아니라 “캐시 키가 달라졌다”는 사실입니다. BuildKit은 각 레이어의 입력(명령, 파일, 메타데이터, 빌드 인자, 마운트, 시크릿 등)을 해시로 계산해 캐시를 재사용합니다. 입력이 조금이라도 달라지면 같은 RUN 이라도 캐시 히트가 나지 않습니다.
이 글은 BuildKit 환경에서 “왜 내 Docker 빌드는 캐시가 안 먹지?”라는 질문에 대해, 실제로 자주 터지는 원인 8가지를 진단 포인트와 함께 정리합니다.
아래 내용은 로컬 Docker Desktop, 리눅스 Docker Engine, 그리고 GitHub Actions 같은 CI에서도 그대로 적용됩니다. CI/CD 관점에서 컨테이너 배포 파이프라인을 운영 중이라면 GitHub Actions로 EKS 무중단 배포 - Blue-Green CI/CD 글도 함께 보면 “빌드 캐시”가 배포 속도에 얼마나 큰 영향을 주는지 체감하기 쉽습니다.
BuildKit 캐시를 확인하는 기본 도구
원인 파악 전에, BuildKit이 실제로 캐시를 쓰고 있는지부터 확인해야 합니다.
1) 빌드 로그에서 캐시 히트 확인
DOCKER_BUILDKIT=1 docker build --progress=plain -t app:dev .
--progress=plain 을 쓰면 각 스텝이 CACHED 인지, 새로 실행되는지 텍스트로 명확히 보입니다.
2) 빌더/캐시 상태 확인
docker buildx ls
buildx 를 사용 중이면 어떤 빌더 인스턴스가 활성인지(로컬 도커 데몬인지, 별도 컨테이너 빌더인지) 확인합니다.
캐시 용량이나 레코드가 비정상적으로 커져 정리해야 할 때는 아래처럼 봅니다.
docker buildx du
정리(주의: 캐시가 날아가 빌드가 느려질 수 있음):
docker buildx prune -f
원인 1) 빌드 컨텍스트에 불필요한 파일이 들어와 매번 해시가 변함
BuildKit은 COPY . . 같은 스텝에서 “컨텍스트에 포함된 파일”을 기반으로 캐시 키를 잡습니다. .git/, node_modules/, dist/, 로그 파일, 로컬 .env 등이 섞이면 파일 변경이 잦아져 캐시가 계속 깨집니다.
진단
COPY . .이후 레이어부터 캐시가 전부 미스- CI에서는 항상 미스, 로컬에서는 가끔 히트
해결
.dockerignore 를 적극적으로 사용합니다.
.git
node_modules
dist
build
coverage
*.log
.env
.DS_Store
그리고 “자주 바뀌는 파일”을 뒤로 미루는 Dockerfile 구조 최적화도 같이 적용합니다.
원인 2) Dockerfile 레이어 순서가 비효율적이라 초반 레이어가 자주 깨짐
캐시는 위에서부터 순차적으로 적용됩니다. 초반 레이어가 깨지면 아래 레이어는 줄줄이 새로 빌드됩니다.
안 좋은 예
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
소스 파일이 조금만 바뀌어도 COPY . . 가 깨지고, npm ci 도 매번 다시 돕니다.
좋은 예
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
의존성 파일이 바뀌지 않으면 npm ci 레이어가 캐시 히트합니다.
원인 3) ARG 값이 매번 바뀌어 캐시 키를 깨뜨림
BuildKit에서 ARG 는 캐시 키에 포함됩니다. CI에서 --build-arg BUILD_DATE=$(date) 같은 값을 넣으면, 그 ARG 를 참조하는 이후 레이어는 매번 재빌드됩니다.
진단
- 빌드 로그에서
ARG선언 이후부터 캐시 미스 - 커밋이 동일한데도 매번 풀 빌드
해결
- 정말 필요한 곳에서만
ARG를 사용 - 빌드 시간/커밋 해시 같은 메타는 이미지 레이어가 아니라 라벨로 최소화
ARG GIT_SHA
LABEL org.opencontainers.image.revision=$GIT_SHA
주의할 점은 LABEL 도 레이어를 만들기 때문에 캐시 관점에서는 영향이 있습니다. 다만 “의존성 설치 레이어”보다 아래쪽에 두면 피해를 줄일 수 있습니다.
원인 4) RUN apt-get update 같은 비결정적 명령이 캐시를 무력화
apt-get update 는 외부 저장소 상태에 따라 결과가 달라질 수 있습니다. BuildKit이 레이어 캐시를 재사용하더라도, 다음 빌드에서 “최신 패키지”를 기대하는 운영 방식이라면 캐시가 오히려 위험할 수 있습니다.
하지만 문제는 그 반대도 있습니다. 어떤 팀은 “항상 최신 패키지”를 위해 의도적으로 --no-cache 를 켜거나, 레이어를 깨는 트릭을 넣어두고 그걸 잊어버립니다.
해결 팁
- OS 패키지는 가능한 고정 버전 사용
- 업데이트와 설치를 한 레이어에서 처리
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
캐시를 잘 쓰면서도 보안 업데이트 정책을 가져가려면, 베이스 이미지를 주기적으로 업데이트하고(예: 주간), 애플리케이션 레이어는 캐시를 최대한 활용하는 전략이 현실적입니다.
원인 5) BuildKit 빌더가 바뀌어 캐시 저장소가 달라짐
로컬에서는 기본 도커 데몬 빌더를 쓰다가, CI에서는 buildx 의 다른 드라이버(예: docker-container)를 쓰면 캐시가 공유되지 않습니다. 같은 Dockerfile이라도 “캐시가 저장된 위치”가 다르면 캐시 히트가 날 수 없습니다.
진단
docker buildx ls
여기서 활성 빌더가 매번 다르거나, CI 실행마다 새 빌더가 생성되면 캐시가 누적되지 않습니다.
해결
- CI에서는 고정된 빌더 인스턴스를 사용
- 원격 캐시(레지스트리 캐시) 사용
예시(레지스트리 캐시):
docker buildx build \
--cache-from type=registry,ref=registry.example.com/app:buildcache \
--cache-to type=registry,ref=registry.example.com/app:buildcache,mode=max \
-t registry.example.com/app:sha-123 \
--push .
이 방식은 GitHub Actions 같은 휘발성 러너에서 특히 효과가 큽니다.
원인 6) COPY 대상 파일의 타임스탬프/퍼미션 변화로 캐시가 깨짐
BuildKit은 파일 내용뿐 아니라 메타데이터도 캐시 키에 영향을 줄 수 있습니다. CI에서 아카이브를 풀거나 체크아웃 방식이 바뀌면 퍼미션/라인엔딩/타임스탬프가 달라져 COPY 레이어가 깨지는 케이스가 있습니다.
진단
- 소스 내용은 동일한데
COPY레이어가 미스 - 특정 CI 단계(압축 해제, 권한 변경) 이후부터 캐시가 안 먹음
해결
- CI에서 소스 준비 과정을 단순화(가능하면
git checkout그대로 사용) - 불필요한
chmod -R같은 작업을 Docker build 컨텍스트 밖에서 하지 않기
권한이 필요한 파일만 선택적으로 처리합니다.
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
원인 7) RUN 단계에서 네트워크/외부 리소스 의존(예: curl 로 다운로드)
RUN curl https://... 처럼 외부에서 파일을 내려받는 빌드는 재현성도 떨어지고, 캐시 전략도 꼬이기 쉽습니다.
- URL이 같아도 서버가 다른 내용을 내려줄 수 있음
- 리다이렉트/헤더/토큰에 따라 결과가 달라짐
해결
- 가능하면 빌드 컨텍스트에 포함하거나(버전 고정)
- 패키지 매니저를 통해 버전 고정
- BuildKit의 캐시 마운트를 적절히 사용
예: Go 모듈 캐시를 BuildKit 캐시 마운트로 유지
# 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 app ./cmd/app
이렇게 하면 소스가 바뀌어도 모듈 다운로드 캐시는 상당 부분 재사용됩니다.
원인 8) 시크릿/SSH 마운트 사용 방식 때문에 캐시가 의도치 않게 무력화
BuildKit의 --mount=type=secret 이나 --mount=type=ssh 는 민감정보를 이미지 레이어에 남기지 않기 위한 기능입니다. 이 자체는 좋은데, 사용 방식에 따라 캐시가 잘 안 먹는 것처럼 보일 수 있습니다.
예를 들어 private repo 의존성을 설치하기 위해 SSH를 쓰는 RUN 스텝은 환경/키/네트워크 상태에 민감하고, 팀마다 “보안상 캐시를 안 쓰게” 설정하는 경우도 있습니다.
권장 패턴
- 시크릿은 필요한
RUN한 스텝에서만 사용 - 의존성 설치를 가능한 한 결정적으로 만들기
# syntax=docker/dockerfile:1.7
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN \
npm ci
COPY . .
RUN npm run build
빌드 시:
docker build \
--secret id=npmrc,src=$HOME/.npmrc \
--progress=plain \
-t app:dev .
이 패턴은 보안과 캐시 효율을 같이 가져가기 좋습니다.
실전 체크리스트: “캐시가 안 먹는다”를 5분 안에 좁히는 순서
docker build --progress=plain으로 어느 스텝부터 캐시가 깨지는지 확인- 그 스텝이
COPY . .라면.dockerignore와 컨텍스트 파일 변화를 의심 - 그 스텝이
ARG이후라면 빌드 인자 변동 여부 확인 - CI라면
buildx빌더가 매번 새로 만들어지는지 확인 - 외부 다운로드/패키지 업데이트가 있으면 “결정적 빌드”로 바꿀 수 있는지 검토
캐시 문제는 성능 문제이기도 하지만, 재현성 문제이기도 합니다. 재현성이 깨지면 운영 장애로 이어질 확률이 커집니다. 예를 들어 배포 환경에서 이미지 풀/레지스트리 인증이 꼬이면 전혀 다른 증상으로 보이기도 하는데, 그런 케이스는 EKS Pod에서 AWS ECR 403 AccessDenied 해결 같은 글의 트러블슈팅 흐름이 도움이 됩니다.
마무리: 캐시는 “우연히 먹는 것”이 아니라 “설계하는 것”
BuildKit 캐시는 똑똑하지만, 입력이 조금만 바뀌어도 다른 결과로 취급합니다. 결국 빠른 빌드는 Dockerfile을 “캐시 친화적으로” 설계하고, CI에서 “캐시가 저장되는 위치”를 안정적으로 유지하며, 외부 의존성을 “결정적으로” 만드는 데서 나옵니다.
정리하면, 캐시 미적용의 대부분은 다음 3가지로 귀결됩니다.
- 컨텍스트가 불안정하다(
.dockerignore미흡, 파일 메타 변경) - 빌드 파라미터가 불안정하다(
ARG, 시크릿/SSH, 빌더 인스턴스) - 빌드가 비결정적이다(
apt-get update, 외부 다운로드)
위 8가지를 순서대로 점검하면, “왜 캐시가 안 먹는지”를 감이 아니라 증거 기반으로 좁힐 수 있습니다.